Introducing Kubed: A Visualization DSL for JavaFX

Whenever I search for visualizations, or look to see how some interesting visualization I’ve come across on the web was created, inevitably it seems I’m lead to D3.js. However, I generally need to create visualizations for Java desktop applications, and while Javascript/Java interoperability has improved over the years embedding a browser in a JavaFX application isn’t an ideal solution — particularly when interaction is required. This approach has worked for some, and others have started improving this interoperability by building a wrapper around D3; but what I’ve always really wanted was a D3-like library which works directly with the JavaFX scene graph.

I’ve started to create such a library a time or two myself but have always abandoned the project quickly when seeing just how verbose/cumbersome the API would become — particularly when stuck using Java 7 (pre-lambdas)! However, I was recently reading about a new language from JetBrains called Kotlin, which has a number of features that make it a good candidate as a host language for embedded DSLs.

So I thought why not learn Kotlin, JavaScript, and D3 by attempting to create a D3-like data visualization DSL embedded within Kotlin that manipulates the JavaFX scene graph — and thus Kubed was born!

So what does Kubed offer? Almost all of the features found in d3-chord, d3-color, d3-ease, d3-hcg, d3-hsv, d3-interpolate, d3-path, d3-scale, d3-scale-chromatic, d3-selection, d3-shape, d3-transition, and most recently d3-geo with a few of the projections from d3-geo-projection sprinkled in.

Time for an example! Lets make a simple bar chart:

val data = listOf(4.0, 8.0, 15.0, 16.0, 23.0. 28.0)
val width = 420.0
val barHeight = 20.0
val x = scaleLinear<Double> {
domain(0.0, data.max()!!)
range(0.0, width)
}
val chart = Group()
val rect = rect<Double> {
width { d, _ -> x(d) }
height(barHeight - 1)
translateY { _, i, _ -> i * barHeight }
}
chart.selectAll<Double>() {
.data(data)
.enter()
.append { d, i, _ -> rect(d, i) }
}

Although this is a simple example, it leverages several features of Kubed including scales, shape generators, and selections.

Lets break down the example. First we create a scale:

val x = scaleLinear<Double> {
domain(0.0, data.max()!!)
range(0.0, width)
}

Here we’ve created a linear scale x, whose domain is mapped from 0.0 to the maximum value in our data (28.0), and whose range is mapped from 0.0 to width. Now we can use x to map from a value in our data set to it’s width in the visualization. For example, x(28.0) would return 420.0 (the entire width), and x(14.0) would return 210.0.You can read more about scales and their varied uses here.

Second, we create a shape generator:

val rect = rect<Double> {
width { d, _ -> x(d) }
height(barHeight - 1)
translateY { _, i, _ -> i * barHeight }
}

Shape generators generate JavaFX Shape instances based on backing data. In this example, rect is a rectangle shape generator, that given some data, of typeDouble, will generate a rectangular shape. We configure three visual properties: width, height, and translateY. Height is set to a constant value for all generated shapes: barHeight — 1 but width and translateY are quite a bit more interesting; with both of these values changing based on the data that the generator is generating a shape for.

Lastly, we create a selection, perform a data join and append rectangles on to the scene graph for each new piece of data.

chart.selectAll<Double>() {
.data(data)
.enter()
.append { d, i, _ -> rect(d, i) }
}

Selections are a big topic, if you aren’t already familiar with them I strongly suggest reading How Selections Work by Mike Bostock. While some of the implementation details are obviously different, the concepts are the same.

Hopefully, for those of you that have worked with D3 before, this looks familiar. The Kubed DSL closely follows D3, varying as needed to support a strongly type programming language, the JavaFX scene graph, and to do things in a idiomatic way for Kotlin developers. For comparison, here is the D3 code for generating the same chart:

var data = [4, 8, 15, 16, 23, 28];
var width = 420,
barHeight = 20;
var x = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, width]);
var chart = d3.select(".chart")
.attr("width", width)
.attr("height", barHeight * data.length);
chart.selectAll("rect")
.data(data)
.enter().append("rect")
.attr("width", x)
.attr("height", barHeight - 1)
.attr("transform", function(d, i) {
return "translate(0," + i * barHeight + ")";
});

This and many other examples (including the code for all visualizations shown at the top of this article) can found in the Kubed GitHub repository. Perhaps my favorite example is ManyPointsDemo.kt, which is a port of Paul Beshai’s block: Animating thousands of points with canvas and D3. The Kubed version doesn’t use a JavaFX canvas, but performs well regardless (canvas support coming soon!)

One of the most exciting recent additions to Kubed is support for geospatial visualizations. While work on this is still on going, the results are promising.

Let’s make a map!

val root = Group()
val width = 960.0
val height = 960.0
val projection = orthographic {
scale = 475.0
translate = doubleArrayOf(width / 2, height / 2)
clipAngle = 90.0
rotate = doubleArrayOf(90.0, -10.0)
}
val path = geoPath(projection)
val url = javaClass.getResource("/world.json")
root.children += path(graticule().graticule()).apply {
stroke = Color.rgb(119, 119, 119, .5)
strokeWidth = 0.5
}
geoJson(url) { geo: GeoJson ->
root.children += path(geo).apply {
fill = Color.BLACK
}
}
😱

If you think a see through spinning globe would be even more exciting, check out SeeThroughGlobeDemo.kt:

Kubed is available now, with beta version 0.1.0 being released to maven repositories this week. I’ll be adding more features, tweaking the DSL, cleaning up some code, and perhaps most importantly — adding documentation as Kubed approaches a 1.0.0 release!

Please give it a try, provide feedback, and report bugs!


I’ve started a series of articles describing how to make visualizations using Kubed; the first of which can be viewed here: Let’s Make a Bar Chart.