How-to do a line chart?

This guide presents how to build a line chart and how to tune it to your needs.

This guide will help you to:

  1. Create your first line chart
  2. Discover all the parameters of a line chart
  3. Style your line chart

What do you need?

  • A working environment of your choice (Android, Kotlin/JS, JavaFX)
  • Intellij Community or Ultimate (tested with IntelliJ 2021.1.*)

To provide runnable examples, all the code in this guide runs on Kotlin/JS. However, Charts.kt code is 100% multiplatform, just paste this code in your own project to obtain the same result.

Your first line chart

Let's say that you just want to display a line chart of several records of temperatures other the day, in the simplest form possible: a List<Double>.

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, 12.3, 12.6, 12.6, 13.2, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) } } }

Let's detail this code:

The data

Your dataset temperatures is a List<Double>, hard to make it simpler!

This means that when you call the chart(temperatures) function, it instantiates a Chart<Double>.

Your chart domain object is a simple Double.

The "temperature" dimension

You create a quantitative dimension of your domain object. The name provided gives a good explanation of the nature of this dimension.

A quantitative dimension accepts only Double values, so you can feed it directly with your domain object.

This creates a continuous numeric dimension that then, can be used in marks to create a continuous numeric axis.

The "hour" dimension

The other dimension is a discrete dimension, and, as you don't have any other value than your temperature, this dimension is based on the indexInData property.

This property returns an Int which is the index of our record in the dataset. By chance, this is exactly what we want to display here.

The "line" mark

This is what makes your chart a line chart. The line mark takes 2 dimensions, for X and Y, and draws segments between each data value.

That's it, you just created your first line chart!

LineMark properties

To get a complete view of all properties, check the LineMark reference page.

The "curve" property

By default, the line mark draws segments between your data points, but there are other options available with the MarkCurves enum.

  • Straight: draw straight lines (default option)
  • Curved: draw splines
  • Step: draw step lines

Uncomment and run the code below to see the differences:

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, 12.3, 12.6, 12.6, 13.2, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) { // The default value is curve = MarkCurves.Straight // Uncomment these lines to test the curve property: // curve = MarkCurves.Curved // curve = MarkCurves.Step } } } }

Joining missing values

Let's say that your temperature reader encounters a problem during the day and several records were missing, giving you a null temperature for the given hour.

Change the joinMissingValues property to see the different behaviors:

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, null, null, null, null, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) { // The default value is joinMissingValues = false // Uncomment these lines to test the joinMissingValues property: //joinMissingValues = true } } } }

Show or hide markers

By default, data points are not visible, the line mark just draws segments from one to another.

This can be changed with the showMarkers property:

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, 12.3, 12.6, 12.6, 13.2, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) { // The default is showMarkers = false // Uncomment these lines to test the showMarkers property: //showMarkers = true } } } }

LineMark style

Let's have a look at some styling properties for the line mark, you'll see that most of them are common to a lot of different marks:

Marker size

The size property is a Dimension<DOMAIN, Double?>: each domain object can have its own marker size.

A null size means that no marker is drawn.

Change configuration default in config.mark.markersSize.

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, 12.3, 12.6, 12.6, 13.2, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) { showMarkers = true //The default is size = constant( config.mark.markersSize ) // Uncomment the following to test different size properties: //size = constant( 60.0 ) //size = constant( null ) //size = discrete( { 5.0 * indexInData } ) } } } }

The line strokes

The line mark has several stroke properties:

  • strokeColor, strokeColorHighlight and strokeColorSelect are Dimension<DOMAIN, Color?>
  • strokeWidth, strokeWidthHighlight and strokeWidthSelect are Dimension<DOMAIN, Color?>
  • strokeLine is a Dimension<DOMAIN, DoubleArray?>

You can bind these 7 properties for each data points:

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* import io.data2viz.color.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, 12.3, 12.6, 12.6, 13.2, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) { strokeColor = constant(Colors.Web.red) //strokeColor = discrete( { if (indexInData%3 == 0) Colors.Web.red else Colors.Web.blue } ) strokeWidth = constant(2.0) //strokeWidth = discrete( { indexInData / 3.0 } ) strokeLine = constant(null) //strokeLine = constant(doubleArrayOf(8.0, 8.0)) } } } }

The marker shape

You can also change the marker shape with the marker property.

This property is a Dimension<DOMAIN, Symbols?>, which means that you can change the marker symbol for each data point of the line using one of the Symbols, which are predefined shapes.

import io.data2viz.charts.core.* import io.data2viz.charts.chart.* import io.data2viz.charts.chart.mark.* import io.data2viz.charts.viz.* import io.data2viz.geom.* import io.data2viz.color.* import io.data2viz.shape.* val width = 600.0 val height = 150.0 fun main() { val vc = newVizContainer().apply { size = Size(width, height) } with(vc) { val temperatures = listOf( 5.2, 5.1, 6.1, 6.0, 5.9, 6.2, 6.4, 6.7, 7.8, 9.8, 12.3, 12.6, 12.6, 13.2, 14.6, 14.5, 14.7, 14.3, 13.8, 11.2, 9.4, 8.0, 6.9, 6.3 ) chart(temperatures) { val tempDimension = quantitative( { domain } ) { name = "Temperature in °C" } val hourDimension = discrete( { indexInData } ) { name = "Time of the day" } line(hourDimension, tempDimension) { showMarkers = true size = constant(80.0) // the default is marker = constant(config.mark.markers[0]) // Uncomment these lines to test the marker property: //marker = discrete( { if ((8..20).contains(indexInData)) Symbols.Circle else Symbols.Star } ) } } } }