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

Data

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 Kotlinmultiplatform 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.

Krayon

iOS

Android

Web

Data

Elements

SvgKanvas

HtmlKanvas

AndroidKanvas

ComposeKanvas

CGContextKanvas


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™:

  1. Create a RootElement
  2. Access RootElement as a Selection
  3. Add a LineElement to the RootElement
  4. Update the start and end points of the LineElement
  5. Draw the RootElement to a HtmlKanvas (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™:

  1. Create a RootElement
  2. Access RootElement as a Selection
  3. Selects all LineElements that are children of the RootElement
  4. Associate data with the Selection
  5. Combine each element of the data with a LineElement
  6. Iterate over each item of the data (data is a Pair<Point, Point>) and LineElements
  7. Deconstruct data as start (Point) and end (Point)
  8. Assign the Point values to the LineElement properties (startX, startY, etc.)
  9. Draw the RootElement to a HtmlKanvas (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™:

  1. Extract the width and height of the HTML Canvas as Floats
  2. Create an example dataset (1 through 10, incrementing by 1)
  3. Configure scaling on the x-axis (essentially, mapping dataset indices to the width of the Canvas)
  4. Configure scaling on the y-axis (mapping the range of dataset values to the height of the Canvas)1
  5. Configure the color and width of the bars
  6. Create a RootElement
  7. Access RootElement as a Selection
  8. Selects all LineElements that are children of the RootElement
  9. Associate data with the Selection
  10. Combine each element of the data with a LineElement
  11. Iterate over each item of the data (indexed)
  12. Assign scaled values to the LineElement properties (startX, startY, etc.)
  13. Assign the bar paint (from step 5)
  14. Draw the RootElement to a HtmlKanvas (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: