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:
- Create your first line chart
- Discover all the parameters of a line chart
- 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 splinesStep
: 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 } )
}
}
}
}