Creating your first Kotlin JS chart app, part 3

This third tutorial details charts events and interactions.

This tutorial will help you understand:

  1. What are the different chart events
  2. How-to listen to events
  3. How-to push events

What do you need?

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

The chart events

There are 4 chart events you can bind on, here is a cheat sheet presenting all of them:

Highlight event

  • This event is a Data Event, it takes and returns DOMAIN objects
  • This event is enabled by default, disable it with config.events.highlightMode
  • Listen: chart.onHighlight(listener: (event: HighlightEvent<DOMAIN>) -> Unit)
  • Push: chart.highlight(domains: Collection<DOMAIN>) or chart.highlight(data: Collection<Datum<DOMAIN>>)

Select event

  • This event is a Data Event, it takes and returns DOMAIN objects
  • This event is disabled by default, disable it with config.events.selectionMode
  • Listen: chart.onSelect(listener: (event: SelectEvent<DOMAIN>) -> Unit)
  • Push: chart.select(domains: Collection<DOMAIN>) or chart.select(data: Collection<Datum<DOMAIN>>)

Zoom event

  • This event is a View Event, it applies transformation to the current charting zone
  • This event is disabled by default, disable it with config.events.zoomMode
  • Listen: chart.onZoom(listener: (event: ZoomEvent) -> Unit)
  • Push: chart.zoom(zoomOriginX, zoomOriginY, zoomFactorX, zoomFactorY)

Pan event (scroll event)

  • This event is a View Event, it applies transformation to the current charting zone
  • This event is disabled by default, disable it with config.events.panMode
  • Listen: chart.onPan(listener: (event: PanEvent) -> Unit)
  • Push: chart.pan(panX: Percent, panY: Percent)

Chart events types

As their name implies, data events take and return "data".

You can trigger an on-screen selection by calling chart.select() with a list of DOMAIN objects.

Similarly, when a user selects stuff on screen, you can retrieve the list of underlying domain objects by listening to the SelectEvent.

View events are related to the "view" of your chart, the main drawing zone, here, in blue.

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

View events do not return data but rather indicate how much the view has changed from its initial position.

Receiving and pushing chart events

Listening to events

From the previous "weather app" (see part 2) we want to add 2 input fields inputMin and inputMax to display the min and max temperature of a highlighted item.

Create these fields, then create a function that takes 2 Double and displays their values in the corresponding fields, let's call this function updateFields:

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.newVizContainer import io.data2viz.format.Locales import io.data2viz.geom.Size import kotlinx.browser.document import kotlinx.browser.window import kotlinx.html.* import kotlinx.html.dom.append import org.w3c.dom.HTMLInputElement import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise 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 fun Node.createEventsChart() { 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 } append { div { span { +" Min. temperature: " } input(InputType.text) { id = "inputMin" style = "width: 60px;" value = "0" } span { +" Max. temperature: " } input(InputType.text) { id = "inputMax" style = "width: 60px;" value = "0" } } div { val vc = newVizContainer().apply { size = Size(width, height) } vc.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) { onHighlight { event: HighlightEvent<Weather> -> val domain = event.data.firstOrNull()?.domain updateFields(domain?.lowTemp ?: .0, domain?.highTemp ?: .0) } } } } } } } } private val updateFields: (Double, Double) -> Unit = { min, max -> (document.getElementById("inputMin") as HTMLInputElement).value = min.toString() (document.getElementById("inputMax") as HTMLInputElement).value = max.toString() }

Now plug function on an onHighlight listener.

The HighlightEvent contains a data property, which is here a Collection<Datum<Weather>>.

On a bar chart your visuals don't overlap, meaning you can't highlight 2 different items, so this collection is whether empty or contains a single element. Just take this element and pass the min and max temperature to the callback function.

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.newVizContainer import io.data2viz.format.Locales import io.data2viz.geom.Size import kotlinx.browser.document import kotlinx.browser.window import kotlinx.html.* import kotlinx.html.dom.append import org.w3c.dom.HTMLInputElement import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise 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 fun Node.createEventsChart() { 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 } append { div { span { +" Min. temperature: " } input(InputType.text) { id = "inputMin" style = "width: 60px;" value = "0" } span { +" Max. temperature: " } input(InputType.text) { id = "inputMax" style = "width: 60px;" value = "0" } } div { val vc = newVizContainer().apply { size = Size(width, height) } vc.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) { onHighlight { event: HighlightEvent<Weather> -> val domain = event.data.firstOrNull()?.domain updateFields(domain?.lowTemp ?: .0, domain?.highTemp ?: .0) } } } } } } } } private val updateFields: (Double, Double) -> Unit = { min, max -> (document.getElementById("inputMin") as HTMLInputElement).value = min.toString() (document.getElementById("inputMax") as HTMLInputElement).value = max.toString() }

Create a new EventsChart.kt file and paste this code (click on the "+" icon to see the full code), change once again the call in your client.kt file, then refresh, you should see this result:

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

Pushing events

Let's say that your application handles scrolling so you want to disable pan in your chart, and control it from your app.

Add a second div in your page with 2 buttons: moveLeft and moveRight.

Each of these buttons will "pan" the chart's view by 1/12 (8.33%):

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.newVizContainer import io.data2viz.format.Locales import io.data2viz.geom.Size import io.data2viz.math.pct import kotlinx.browser.document import kotlinx.browser.window import kotlinx.html.* import kotlinx.html.dom.append import kotlinx.html.js.onClickFunction import org.w3c.dom.HTMLInputElement import org.w3c.dom.Node import org.w3c.fetch.Response import kotlin.js.Promise 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 fun Node.createEventsChart() { 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 weatherChart: Chart<Weather> append { div { span { +" Min. temperature: " } input(InputType.text) { id = "inputMin" style = "width: 60px;" value = "0" } span { +" Max. temperature: " } input(InputType.text) { id = "inputMax" style = "width: 60px;" value = "0" } } div { button { id = "moveLeft" text("<< Move the chart left") onClickFunction = { weatherChart.pan((-8.333).pct, 0.pct) } } button { id = "moveRight" text("Move the chart right >>") onClickFunction = { weatherChart.pan(8.3333.pct, 0.pct) } } } div { val vc = newVizContainer().apply { size = Size(width, height) } weatherChart = vc.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) { onHighlight { event: HighlightEvent<Weather> -> val domain = event.data.firstOrNull()?.domain updateFields(domain?.lowTemp ?: .0, domain?.highTemp ?: .0) } } } } } } } } private val updateFields: (Double, Double) -> Unit = { min, max -> (document.getElementById("inputMin") as HTMLInputElement).value = min.toString() (document.getElementById("inputMax") as HTMLInputElement).value = max.toString() }

By default, panning is disabled in your chart: this concerns the user interactions. Your users cannot scroll in the chart, still, you can always call chart.pan() and control the scroll from your application.

Replace your code with the code above and refresh your page, you now should be able to move your chart right and left by clicking on the buttons:

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

For more information about Zoom and Pan, check this guide.