Skip to content

Repository files navigation

womblot

Maven Central License Kotlin Compose Multiplatform

Configurable, data-source-agnostic reporting for Kotlin Multiplatform. Feed it rows, define your dimensions and measures, and render themeable Koala Plot charts — on Android, iOS, desktop, and the web from a single codebase.

  • Agnostic — the engine reads rows through a one-method RowSource. FHIR, SQL, REST, or in-memory: the charts never see your backend.
  • Multiplatform — Android, iOS, JVM/desktop, JS, and WasmJS.
  • Themeable — light/dark themes, ready-made colour palettes, and per-chart style overrides.
  • Extensible — register your own chart types and export formats without forking.

The namewomblot = wombat + plot: a small tribute to Koala Plot, which draws every chart here. The wombat is the koala's closest relative, so the nod stays in the family. 🐨🐻

Screenshots

The demo module running in the browser on WasmJS — the same Compose code that ships to Android, iOS, and desktop.

Light Dark
Metric cards and bar charts in the light theme Target bars, bullet graph, and radar in the dark theme

Supported platforms

Target womblot-core womblot-compose
Android
iOS (iosArm64, iosSimulatorArm64)
JVM / desktop
JS (browser, Node)
WasmJS

Modules

Module Contents
womblot-core The RowSource seam, the aggregation engine (dimensions × measures, date bucketing, filters), and CSV/JSON export. Pure Kotlin, no UI.
womblot-compose Compose Multiplatform visuals (metric cards + charts) drawn with Koala Plot, the ReportVisualRegistry, ReportTheme, and the ReportResult → ChartData adapter.

Installation

Artifacts are published to Maven Central under io.github.ellykits.womblot.

repositories {
  mavenCentral()
}

kotlin {
  sourceSets {
    commonMain.dependencies {
      implementation("io.github.ellykits.womblot:womblot-core:1.0.0-alpha02")
      implementation("io.github.ellykits.womblot:womblot-compose:1.0.0-alpha02")
    }
  }
}

womblot-compose exposes womblot-core as an api dependency, so the ReportResult types the chart adapters take come along with it — depend on womblot-core directly only if you use the engine without any UI.

womblot-core supports all targets and may live in commonMain. womblot-compose targets Android, iOS, desktop, JS, and WasmJS.

Getting started from scratch

This walkthrough builds the exact report the demo module ships with — count of service visits per month, split by programme — so you can follow along against real, runnable code. The three moving parts are always the same: rows in → a definition → a result → a visual.

1. Shape your rows

A Row is just a Map<String, ReportValue> — one flat record. womblot never parses your domain objects; you hand it rows and it aggregates them. For tests and prototypes, InMemoryRowSource groups named lists of rows into "sources" a report can point at.

import io.ellykits.womblot.model.ReportValue
import io.ellykits.womblot.source.InMemoryRowSource
import io.ellykits.womblot.source.Row

fun visit(date: String, program: String, patient: String): Row =
  mapOf(
    "date" to ReportValue.Text(date),
    "program" to ReportValue.Text(program),
    "patient" to ReportValue.Text(patient),
  )

val visits =
  listOf(
    visit("2026-01-05", "ANC", "p1"),
    visit("2026-01-20", "ANC", "p2"),
    visit("2026-01-15", "Immunization", "p3"),
    visit("2026-02-10", "ANC", "p4"),
    visit("2026-02-18", "Immunization", "p6"),
    visit("2026-03-04", "ANC", "p7"),
    visit("2026-03-22", "Immunization", "p9"),
  )

val source = InMemoryRowSource(mapOf("visits" to visits))

For anything real, implement the one-method seam instead — the engine only ever calls rows(query):

class SqlRowSource(private val db: Database) : RowSource {
  override suspend fun rows(query: SourceQuery): List<Row> =
    db.select(query.source).map { record -> record.toReportRow() }
}

2. Describe the report with dimensions and measures

A ReportDefinition is the whole query, declared as data:

  • source — which named row set to read ("visits" above).
  • dimensions — how to group. Each Dimension(name, column, bucket?) reads one column; an optional DateBucket (DAY, WEEK, MONTH, QUARTER, YEAR) collapses a date column into periods. Here date → month, and program is grouped verbatim.
  • measures — what to compute per group. Each Measure(name, aggregation, column?) applies an Aggregation (COUNT, SUM, AVG, MIN, MAX, DISTINCT). COUNT needs no column.
  • filters (optional)Filter(column, op, value) drops rows before aggregation (EQ, NE, GT, LT, …).
import io.ellykits.womblot.model.Aggregation
import io.ellykits.womblot.model.DateBucket
import io.ellykits.womblot.model.Dimension
import io.ellykits.womblot.model.Measure
import io.ellykits.womblot.model.ReportDefinition

val servicesByMonth =
  ReportDefinition(
    name = "servicesByMonth",
    source = "visits",
    dimensions =
      listOf(
        Dimension("month", column = "date", bucket = DateBucket.MONTH),
        Dimension("program", column = "program"),
      ),
    measures = listOf(Measure("count", Aggregation.COUNT)),
  )

3. Run the engine

ReportEngine reads the rows from the source and returns a ReportResult — a small cube of cells keyed by each dimension combination, holding the computed measures. It is pure data, UI-free, and identical on every platform.

import io.ellykits.womblot.engine.ReportEngine

val result = ReportEngine(source).run(servicesByMonth)

4. Render a visual

Wrap content in a ReportThemeProvider, pivot the result into ChartData with toChartData(...), and drop in any visual. category picks the axis dimension, measure the value, and the optional seriesBy splits into a series per value of another dimension (here, one bar colour per programme).

import io.ellykits.womblot.compose.model.toChartData
import io.ellykits.womblot.compose.theme.ReportPalettes
import io.ellykits.womblot.compose.theme.ReportTheme
import io.ellykits.womblot.compose.theme.ReportThemeProvider
import io.ellykits.womblot.compose.ui.Metric
import io.ellykits.womblot.compose.ui.MetricRow
import io.ellykits.womblot.compose.ui.chart.BarChart

ReportThemeProvider(theme = ReportTheme.light(ReportPalettes.Cool)) {
  Column {
    MetricRow(
      listOf(
        Metric("Households visited", "248"),
        Metric("ANC visits", "86"),
      ),
    )
    BarChart(
      data = result.toChartData(category = "month", measure = "count", seriesBy = "program"),
      title = "Services by month",
    )
  }
}

5. Export

val csv: ByteArray = ExportRegistry.export("csv", result)
val json: ByteArray = ExportRegistry.export("json", result)

Visuals

Every visual takes caller-supplied ChartData and a ChartStyle, renders inside a themed card, and is drawn by Koala Plot underneath.

Composable Registry type Notes
MetricCard, MetricRow Headline value with a ↑/↓ delta; MetricRow fills on wide screens and swipes on narrow ones.
BarChart bar, barStacked Grouped or stacked vertical bars.
HorizontalBarChart hbar Ranked horizontal bars.
LineChart line, area One or more series; per-series solid/dashed; optional area fill.
DonutChart donut, pie Slices with a centre total and legend.
TargetBarChart targetBar Progress bars against a target marker.
BulletChart bullet Measure vs. target with qualitative bands.
RadarChart radar Multi-axis comparison across series.
DataTable Columns with threshold-coloured cells and status chips.

Theming

ReportTheme controls the palette, semantic colours, card, axis, and grid. Use ReportTheme.light(palette) / ReportTheme.dark(palette) with a preset from ReportPalettes (Default, Cool, Warm, Pastel, Vibrant, ColorblindSafe), or build your own. Any ChartStyle.palette overrides the theme for a single chart.

ReportThemeProvider(theme = ReportTheme.dark(ReportPalettes.ColorblindSafe)) { /**/ }

Extending

Register a new visual type — configuration and ReportVisualRegistry.Render pick it up like any built-in.

registerBuiltinVisuals()
ReportVisualRegistry.register("gauge") { data, style, title -> MyGauge(data, style, title) }

ExportRegistry.register("xlsx", exporter) adds export formats the same way.

Demo

The demo shows every visual over the sample data above, with a theme switch and a palette picker.

./gradlew :demo:run                          # desktop
./gradlew :demo:wasmJsBrowserDevelopmentRun  # browser, WasmJS

Credits

Charts are rendered with Koala Plot, a Compose Multiplatform plotting library, which womblot is named in tribute to. womblot is a thin, opinionated reporting layer on top of it: the aggregation engine, theming, and config surface are ours; the drawing is theirs. If you need lower-level plotting control, use Koala Plot directly.

License

Apache License 2.0.

About

Configurable, data-source-agnostic reporting for Kotlin Multiplatform. Feed it rows, define your dimensions and measures, and render themeable [Koala Plot](https://github.com/KoalaPlot/koalaplot-core) charts — on Android, iOS, desktop, and the web from a single codebase

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages