This fourth tutorial will explain how charts synchronization works.

This tutorial will help you understand how-to:

  1. Synchronize charts using events
  2. Synchronize multiple charts' layouts using a SizeManager

What do you need?

Synchronizing charts events

When you have, in your application, distinct charts with related data, it may be relevant to synchronize them (axes, selection, highlighted item...).

The standard case is when you want to visualize different dataset values through different charts.

Let's do this and display 3 different cities' weather charts. Start by loading the weather statistics for Chicago, Mumbai, and Auckland, then create a chart for each city.

//run highlight-only=true from=45 to=56
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.scene.layout.VBox
import javafx.stage.Stage
import java.io.File

fun main() {
    Application.launch(MultiChart::class.java)
}

private val width = 500.0
private val height = 160.0

private val months =
    listOf("Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.")

private val year = 2017

private fun loadData(cityName: String): List<Weather> {
    val csvContent = File("./src/main/resources/Weather - Stats.csv").readText()
    val weatherData = parseWeatherCSV(csvContent)
    val filteredData = weatherData.filter { weather: Weather ->
        weather.city == cityName && weather.year == year
    }
    return filteredData
}

class MultiChart : Application() {

    override fun start(stage: Stage) {

        val root = VBox()
        
        val citiesNames = listOf("Chicago", "Mumbai", "Auckland")
        val citiesWeather = citiesNames.map { loadData(it) }
        
        val chartPanes = citiesNames.map { Pane() }
        root.children.addAll(chartPanes)

        val charts = citiesNames.mapIndexed { index, cityName ->
            chartPanes[index].newVizContainer().run {
                size = Size(width, height)
                createChart(citiesWeather[index], cityName)
            }
        }

        // Launch our Scene
        stage.apply {
            title = "Chicago average temperature in 2017"
            scene = (Scene(root, width, height))
            show()
        }
    }

    private fun VizContainer.createChart(chicago2017: List<Weather>, cityName: String): Chart<Weather> =
        chart(chicago2017) {

            val monthDimension = discrete({ domain.month }) {
                formatter = { months[this - 1] }
            }
            val temperatureDimension = quantitative({ domain.avgTemp }) {
                name = "Avg temp for $cityName"
                formatter = { "${this.formatToInteger(Locales.en_US)} °F" }
            }

            bar(monthDimension, temperatureDimension)
        }
}

Copy this code (get the whole code by hitting "+") in a MultiChart.kt file, running the main() gives this result:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/66d1f01c-7560-4f65-8a9a-f8ea4fbc5b9d/Untitled.png

Now, synchronizing events for these 3 charts is just as easy as it seems: listen for an event on a chart and propagate it on the 2 others.

Note that you can't just "pass" the event, you have to decompose it, but all useful information is already present:

//run highlight-only=true from=59 to=65
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.event.HighlightEvent
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.scene.layout.VBox
import javafx.stage.Stage
import java.io.File

fun main() {
    Application.launch(MultiChart::class.java)
}

private val width = 500.0
private val height = 160.0

private val months =
    listOf("Jan.", "Feb.", "Mar.", "Apr.", "May", "Jun.", "Jul.", "Aug.", "Sep.", "Oct.", "Nov.", "Dec.")

private val year = 2017

private fun loadData(cityName: String): List<Weather> {
    val csvContent = File("./src/main/resources/Weather - Stats.csv").readText()
    val weatherData = parseWeatherCSV(csvContent)
    val filteredData = weatherData.filter { weather: Weather ->
        weather.city == cityName && weather.year == year
    }
    return filteredData
}

class MultiChart : Application() {

    override fun start(stage: Stage) {

        val root = VBox()

        val citiesNames = listOf("Chicago", "Mumbai", "Auckland")
        val citiesWeather = citiesNames.map { loadData(it) }

        val chartPanes = citiesNames.map { Pane() }
        root.children.addAll(chartPanes)

        val charts = citiesNames.mapIndexed { index, cityName ->
            chartPanes[index].newVizContainer().run {
                size = Size(width, height)
                createChart(citiesWeather[index], cityName)
            }
        }

        charts.forEachIndexed { index, chart ->
            chart.onHighlight { event: HighlightEvent<Weather> ->
            if (index != 0) charts[0].highlight(event.data)
                if (index != 1) charts[1].highlight(event.data)
                if (index != 2) charts[2].highlight(event.data)
            }
        }

        // Launch our Scene
        stage.apply {
            title = "Chicago average temperature in 2017"
            scene = (Scene(root, width, height))
            show()
        }
    }

    private fun VizContainer.createChart(chicago2017: List<Weather>, cityName: String): Chart<Weather> =
        chart(chicago2017) {

            val monthDimension = discrete({ domain.month }) {
                formatter = { months[this - 1] }
            }
            val temperatureDimension = quantitative({ domain.avgTemp }) {
                name = "Avg temp for $cityName"
                formatter = { "${this.formatToInteger(Locales.en_US)} °F" }
            }

            bar(monthDimension, temperatureDimension)
        }
}

Get the whole code and paste it in your file, run the main(), you should see now that the highlight event is synchronized between the 3 charts:

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/4dcaa4a0-d834-465d-bb8a-37044497ab9e/Untitled.png