Creating your first Kotlin JS chart app, part 4

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?

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

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.

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 kotlinx.browser.window import kotlinx.html.div import kotlinx.html.dom.append import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise 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 val citiesNames = listOf("Chicago", "Mumbai", "Auckland") fun Node.createMultiChart() { val request: Promise<Response> = window.fetch("https://docs.google.com/spreadsheets/d/e/2PACX-IKQ5BVyEgowXG_qJ/pub?gid=650761999&single=true&output=csv") request.then { response -> response.text().then { csvContent -> val weatherData = parseWeatherCSV(csvContent) val citiesWeather = citiesNames.map { filterData(weatherData, it) } append { val charts = citiesNames.mapIndexed { index, cityName -> div { newVizContainer().run { size = Size(width, height) buildChart(citiesWeather[index], cityName) } } } } } } } private fun filterData(weatherData: List<Weather>, cityName: String): List<Weather> = weatherData.filter { weather: Weather -> weather.city == cityName && weather.year == year } private fun VizContainer.buildChart(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, call the createMultiChart() function from your client.kt file and you should see this:

Creating%20your%20first%20Kotlin%20JS%20chart%20app,%20part%204/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:

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 kotlinx.browser.window import kotlinx.html.div import kotlinx.html.dom.append import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise 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 val citiesNames = listOf("Chicago", "Mumbai", "Auckland") fun Node.createMultiChart() { val request: Promise<Response> = window.fetch("https://docs.google.com/spreadsheets/d/e/2PACX-IKQ5BVyEgowXG_qJ/pub?gid=650761999&single=true&output=csv") request.then { response -> response.text().then { csvContent -> val weatherData = parseWeatherCSV(csvContent) val citiesWeather = citiesNames.map { filterData(weatherData, it) } append { val charts: MutableList<Chart<Weather>> = mutableListOf() citiesNames.mapIndexed { index, cityName -> div { newVizContainer().run { size = Size(width, height) charts += buildChart(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) } } } } } } private fun filterData(weatherData: List<Weather>, cityName: String): List<Weather> = weatherData.filter { weather: Weather -> weather.city == cityName && weather.year == year } private fun VizContainer.buildChart(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, refresh, you should see now that the highlight event is synchronized between the 3 charts:

Creating%20your%20first%20Kotlin%20JS%20chart%20app,%20part%204/Untitled%201.png

Synchronizing charts based on different datasets

The previous "standard case" is pretty obvious, but you may encounter more complex setups where you need to do a bit more computation, for example when you want to synchronize 2 charts that did not share the same dataset.

However, synchronizing 2 charts means that they share at least one common value.

This implies that you can retrieve the value from the first chart, filter your second dataset to find the matching collection of items, and call the highlight() function on the second chart with it.

Synchronizing charts layouts

When you want to display synchronized charts, you often want to display them in a horizontal or a vertical layout and expect the axes to be perfectly aligned so you can compare things easily.

Charts-kt manages its own layout: it draws charts by taking into account the available space and all the constraints from the data, the formatters, the style, etc...

Let's create a new file called VerticalCharts.kt and paste this code in it:

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 kotlinx.browser.window import kotlinx.html.div import kotlinx.html.dom.append import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise private val width = 500.0 private val height = 200.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 fun Node.createVerticalChart() { val request: Promise<Response> = window.fetch("https://docs.google.com/spreadsheets/d/e/2PACX-IKQ5BVyEgowXG_qJ/pub?gid=650761999&single=true&output=csv") request.then { response -> response.text().then { csvContent -> val weatherData = parseWeatherCSV(csvContent) val chicago2017 = weatherData.filter { it.city == city && it.year == year } lateinit var temperatureChart: Chart<Weather> lateinit var precipitationChart: Chart<Weather> append { div { temperatureChart = newVizContainer().run { size = Size(width, height) createChart(chicago2017, { avgTemp }, "Average temperature in °F") } } div { precipitationChart = newVizContainer().run { size = Size(width, height) createChart(chicago2017, { precipitation }, "Precipitations in inches") } } } temperatureChart.onHighlight { precipitationChart.highlight(it.data) } precipitationChart.onHighlight { temperatureChart.highlight(it.data) } } } } private fun VizContainer.createChart( chicago2017: List<Weather>, weatherValue: Weather.() -> Double, valueName: String ): Chart<Weather> = chart(chicago2017) { val monthDimension = discrete({ domain.month }) { formatter = { months[this - 1] } } val quantitativeDimension = quantitative({ weatherValue(domain) }) { name = valueName } bar(monthDimension, quantitativeDimension) }

You now have 2 charts, one for the average temperature, and one for the precipitation, call your createVerticalChart() function and refresh, the result should look like this:

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

As the displayed data is different, you can see that the Y-axes labels are also different, this leads to a first Y-axis a bit wider than the second, and so, the 2 axes are not aligned.

To fix this, use a SizeManager to indicate that these 2 charts are meant to be vertically aligned:

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.layout.sizeManager import io.data2viz.charts.viz.VizContainer import io.data2viz.charts.viz.newVizContainer import io.data2viz.geom.Size import kotlinx.browser.window import kotlinx.html.div import kotlinx.html.dom.append import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise private val width = 500.0 private val height = 200.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 fun Node.createVerticalChart() { val request: Promise<Response> = window.fetch("https://docs.google.com/spreadsheets/d/e/2PACX-IKQ5BVyEgowXG_qJ/pub?gid=650761999&single=true&output=csv") request.then { response -> response.text().then { csvContent -> val weatherData = parseWeatherCSV(csvContent) val chicago2017 = weatherData.filter { it.city == city && it.year == year } lateinit var temperatureChart: Chart<Weather> lateinit var precipitationChart: Chart<Weather> append { div { temperatureChart = newVizContainer().run { size = Size(width, height) createChart(chicago2017, { avgTemp }, "Average temperature in °F") } } div { precipitationChart = newVizContainer().run { size = Size(width, height) createChart(chicago2017, { precipitation }, "Precipitations in inches") } } } temperatureChart.onHighlight { precipitationChart.highlight(it.data) } precipitationChart.onHighlight { temperatureChart.highlight(it.data) } val verticalSynchronizer = sizeManager().vSynchro() verticalSynchronizer.addAllCharts(temperatureChart, precipitationChart) } } } private fun VizContainer.createChart( chicago2017: List<Weather>, weatherValue: Weather.() -> Double, valueName: String ): Chart<Weather> = chart(chicago2017) { val monthDimension = discrete({ domain.month }) { formatter = { months[this - 1] } } val quantitativeDimension = quantitative({ weatherValue(domain) }) { name = valueName } bar(monthDimension, quantitativeDimension) }

Save, refresh and check the result, as you can see the charts now use the expected layout:

Creating%20your%20first%20Kotlin%20JS%20chart%20app,%20part%204/Untitled%203.png

Find more information about chart's layout management in this guide.