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 Element
s
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
RootElement
as aSelection
- Add a
LineElement
to theRootElement
- Update the start and end points of the
LineElement
- Draw the
RootElement
to 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
RootElement
as aSelection
- Selects all
LineElement
s 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 (
data
is aPair<Point, Point>
) andLineElement
s - Deconstruct
data
asstart
(Point
) andend
(Point
) - Assign the
Point
values to theLineElement
properties (startX
,startY
, etc.) - Draw the
RootElement
to 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
width
andheight
of the HTML Canvas asFloat
s - 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
RootElement
as aSelection
- Selects all
LineElement
s 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
LineElement
properties (startX
,startY
, etc.) - Assign the bar paint (from step 5)
- Draw the
RootElement
to 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: