Tutorial
Comparison to D3
D3 (or D3.js) is a free, open-source JavaScript library for visualizing data.
D3 (which stands for “Data-Driven Documents”) is commonly used to have a dataset drive manipulation of the HTML Document Object Model (DOM).
D3’s “most central feature” is “data binding”, whereas data can drive the mutation of the HTML DOM. D3 can also be used to draw to SVG or Canvas, but faux elements (rather than HTML DOM elements) are created to take advantage of D3’s data binding capabilities.
D3 is undoubtedly a powerful visualization library, but it only works in JavaScript environments (namely, the Web).
Enter Krayon, a Kotlin™ multiplatform library that aims to bring powerful visualization tools to the many platforms supported by Kotlin™ (e.g. Android, iOS, Web).
Elements
Since Android and iOS don’t have an HTML DOM, Krayon provides intermediary Elements
that can undergo data binding and be rendered on supported platforms.
Drawing
To draw a simple line on an HTML Canvas, we start by creating a Canvas:
<canvas id="canvas1" width="100" height="100" style="outline: black 1px solid;"></canvas>
We can then perform the following in Kotlin™:
- Create a
RootElement - Access
RootElementas aSelection - Add a
LineElementto theRootElement - Update the start and end points of the
LineElement - Draw the
RootElementto aHtmlKanvas(which bridges to an HTML Canvas)
package tutorial
import com.juul.krayon.element.LineElement
import com.juul.krayon.element.RootElement
import com.juul.krayon.kanvas.HtmlKanvas
import com.juul.krayon.selection.append
import com.juul.krayon.selection.asSelection
import com.juul.krayon.selection.each
import org.w3c.dom.HTMLCanvasElement
@JsExport
fun setupLine1(element: HTMLCanvasElement) {
val root = RootElement() // 1
root.asSelection() // 2
.append(LineElement) // 3
.each {
startX = 10f // 4
startY = 10f // 4
endX = 90f // 4
endY = 90f // 4
}
root.draw(HtmlKanvas(element)) // 5
}
We can simply call into the JavaScript API produced by Kotlin (via @JsExport):
<script>sample.tutorial.setupLine1(document.getElementById("canvas1"));</script>
This will render as:
Data
To draw the same line as above, but powered by a dataset, we can perform the following in Kotlin™:
- Create a
RootElement - Access
RootElementas aSelection - Selects all
LineElements that are children of theRootElement - Associate data with the
Selection - Combine each element of the data with a
LineElement - Iterate over each item of the data (
datais aPair<Point, Point>) andLineElements - Deconstruct
dataasstart(Point) andend(Point) - Assign the
Pointvalues to theLineElementproperties (startX,startY, etc.) - Draw the
RootElementto aHtmlKanvas(which bridges to an HTML Canvas)
package tutorial
import com.juul.krayon.element.LineElement
import com.juul.krayon.element.RootElement
import com.juul.krayon.kanvas.HtmlKanvas
import com.juul.krayon.selection.asSelection
import com.juul.krayon.selection.data
import com.juul.krayon.selection.each
import com.juul.krayon.selection.join
import com.juul.krayon.selection.selectAll
import org.w3c.dom.HTMLCanvasElement
private data class Point(
val x: Float,
val y: Float,
)
@JsExport
fun setupLine2(element: HTMLCanvasElement) {
val root = RootElement() // 1
root.asSelection() // 2
.selectAll(LineElement) // 3
.data(listOf(Point(10f, 10f) to Point(90f, 90f))) // 4
.join(LineElement) // 5
.each { (data) -> // 6
val (start, end) = data // 7
startX = start.x // 8
startY = start.y // 8
endX = end.x // 8
endY = end.y // 8
}
root.draw(HtmlKanvas(element)) // 9
}
This will render as:
Bar Chart
We can use a larger dataset with the LineElement to create a simple bar chart. We start by
creating a larger HTML Canvas:
<canvas id="barchart" width="250" height="100" style="outline: black 1px solid;"></canvas>
Then we can construct the bar chart in Kotlin™:
- Extract the
widthandheightof the HTML Canvas asFloats - Create an example dataset (1 through 10, incrementing by 1)
- Configure scaling on the x-axis (essentially, mapping dataset indices to the width of the Canvas)
- Configure scaling on the y-axis (mapping the range of dataset values to the height of the Canvas)1
- Configure the color and width of the bars
- Create a
RootElement - Access
RootElementas aSelection - Selects all
LineElements that are children of theRootElement - Associate data with the
Selection - Combine each element of the data with a
LineElement - Iterate over each item of the data (indexed)
- Assign scaled values to the
LineElementproperties (startX,startY, etc.) - Assign the bar paint (from step 5)
- Draw the
RootElementto aHtmlKanvas(which bridges to an HTML Canvas)
1 Note that for step 4 the order of the range values are flipped (with height listed
before 0f). This is to invert y-axis rendering, since the rendering origin is the upper left
corner (with y increasing downward).
package tutorial
import com.juul.krayon.color.blue
import com.juul.krayon.element.LineElement
import com.juul.krayon.element.RootElement
import com.juul.krayon.kanvas.HtmlKanvas
import com.juul.krayon.kanvas.Paint
import com.juul.krayon.scale.domain
import com.juul.krayon.scale.range
import com.juul.krayon.scale.scale
import com.juul.krayon.selection.asSelection
import com.juul.krayon.selection.data
import com.juul.krayon.selection.each
import com.juul.krayon.selection.join
import com.juul.krayon.selection.selectAll
import org.w3c.dom.HTMLCanvasElement
@JsExport
fun setupBarChart(element: HTMLCanvasElement) {
val (width, height) = element.size // 1
val data = (1..10).toList() // 2
val x = scale() // 3
.domain(0, data.count() - 1) // 3
.range(0f, width) // 3
val y = scale() // 4
.domain(0, data.max()) // 4
.range(height, 0f) // 4
val barPaint = Paint.Stroke( // 5
color = blue, // 5
width = x.scale(1), // 5
)
val root = RootElement() // 6
root.asSelection() // 7
.selectAll(LineElement) // 8
.data(data) // 9
.join(LineElement) // 10
.each { (data, index) -> // 11
startX = x.scale(index) // 12
startY = y.scale(0) // 12
endX = x.scale(index) // 12
endY = y.scale(data) // 12
paint = barPaint // 13
}
root.draw(HtmlKanvas(element)) // 14
}
private val HTMLCanvasElement.size
get() = width.toFloat() to height.toFloat()
This will render as: