Skip to content
HDCharts

3.0.0 Migration

Upgrade note: If upgrading from 2.4.0 or earlier, read these migration notes.

Bar and Histogram v3 migration

Use ChartData with one Double series for both BarChart and HistogramChart. Convert older dataset values before creating the chart data.

Before

BarChart(
    dataSet = listOf(18f, 32f, 26f).toChartDataSet(
        title = "Daily sales",
        labels = listOf("Mon", "Tue", "Wed"),
    ),
    selectedBarIndex = 1,
)

After

val selection = rememberChartSelection(initialIndex = 1)

BarChart(
    data = listOf(18.0, 32.0, 26.0).toChartData(
        categories = listOf("Mon", "Tue", "Wed"),
        seriesName = "Daily sales",
    ),
    title = "Daily sales",
    selection = selection,
)

The equivalent HistogramChart migration is:

// Before
HistogramChart(
    dataSet = listOf(3f, 6f, 11f, 16f, 14f, 9f, 5f).toChartDataSet(
        title = "Request Duration Distribution",
        labels = listOf("0-50ms", "50-100ms", "100-150ms", "150-200ms", "200-250ms", "250-300ms", "300ms+"),
    ),
)

// After
HistogramChart(
    data = listOf(3.0, 6.0, 11.0, 16.0, 14.0, 9.0, 5.0).toChartData(
        categories = listOf("0-50ms", "50-100ms", "100-150ms", "150-200ms", "200-250ms", "250-300ms", "300ms+"),
        seriesName = "Request Duration Distribution",
    ),
    title = "Request Duration Distribution",
)

Both charts accept one series. Histogram values must be nonnegative. The old dataSet overloads and selectedBarIndex parameter are removed; use the top-level selection parameter instead.

Styles

Styles are grouped. Migrate customizations to the corresponding blocks on BarChartStyle or HistogramChartStyle, such as bars, range, grid, axis, and selection. Create bar styles with BarChartDefaults and histogram styles with HistogramChartDefaults, rather than the removed flat style parameters.

Selecting a bar dims the other bars to 70% opacity, and the selection line is drawn only outside the selected bar. Pass selection(unselectedAlpha = 1f) to keep every bar solid.

Grid, axis, and selection line widths default to 1.dp. In 2.x they were one pixel, so they now look heavier on high-density screens.

Behavior

  • title is separate from seriesName and category labels.
  • An empty category list hides X-axis labels; supplied categories must match the value count.
  • selectedValueFormatter formats selected values and axisValueFormatter formats Y-axis ticks independently.
  • Selection identifies a source bar or bin. Replacing the data clears it; resizing and density changes preserve it.
  • Set interactionEnabled = false to disable user controls while retaining programmatic selection.

Kotlin/JS to Kotlin/Wasm migration

Use Kotlin/Wasm (wasmJs) for web applications that consume HDCharts.

Before

js {
    browser()
}

After

Replace the Kotlin/JS web target with wasmJs:

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
    browser()
}

JVM, Android, and iOS targets are unchanged.

Line v3 migration

Use one LineChart for both single- and multi-series line charts. The chart accepts shared ChartData with aligned Double values.

Before

@Composable
private fun ShowMultiLine() {
    val items = listOf(
        "Web Store" to listOf(420f, 510f, 480f, 530f, 560f, 590f),
        "Mobile App" to listOf(360f, 420f, 410f, 460f, 500f, 540f),
        "Partner Sales" to listOf(280f, 320f, 340f, 360f, 390f, 420f),
    )

    val dataSet = items.toMultiChartDataSet(
        title = "Weekly Revenue by Channel",
        prefix = "$",
        categories = listOf("Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6"),
    )

    LineChart(
        dataSet = dataSet,
        selectedPointIndex = 1,
    )
}

After

val selection = rememberChartSelection(initialIndex = 1)

LineChart(
    data = listOf(
        "Web Store" to listOf(420.0, 510.0, 480.0, 530.0, 560.0, 590.0),
        "Mobile App" to listOf(360.0, 420.0, 410.0, 460.0, 500.0, 540.0),
        "Partner Sales" to listOf(280.0, 320.0, 340.0, 360.0, 390.0, 420.0),
    ).toChartData(
        categories = listOf("Week 1", "Week 2", "Week 3", "Week 4", "Week 5", "Week 6"),
    ),
    title = "Weekly Revenue by Channel",
    selection = selection,
    selectedValueFormatter = ChartValueFormatters.prefix("$"),
)

The old ChartDataSet and MultiChartDataSet entry points are removed. There is no separate multi-line composable. Use chartDataOf when explicit ChartSeries construction is more convenient.

Live line charts

Use LiveLineChart for timeline updates. It replaces renderMode = LineChartRenderMode.Timeline. shiftDuration: Duration replaces animationDurationMillis: Int, so pass 120.milliseconds instead of 120.

LiveLineChart(
    data = readings.toChartData(seriesName = "Sensor"),
    title = "Sensor",
    shiftDuration = 120.milliseconds,
)

LiveLineChart has no selection or gestures, so drop interactionEnabled and selectedPointIndex from calls you move to it. It shows no value readout, so format the Y axis with axisValueFormatter. LineChart always morphs, so drop renderMode = LineChartRenderMode.Morph and animationDurationMillis from its calls.

Styles and selection

Move customizations to the grouped LineChartStyle sections such as line, points, selection, and axis. Use selection instead of selectedPointIndex; it identifies one source X index shared by all series. Use staticChartSelection(index) for a preset preview or screenshot.

Sizes are Dp and scale with screen density. The 2.x Float sizes were pixels, so divide them by the screen density when migrating. For example, pointSize = 9f looked like points(size = 3.dp) on a 3x screen. Point and selection sizes are radii. The defaults are a 2.dp line, 4.dp points, 3.dp and 5.dp selection markers, and a 1.dp axis and selection line.

The selection line uses onSurface, like the other charts. Set its color and width with selection(color, width); the markers use markerColor.

interactionEnabled = false disables user controls, including fit/expand, zoom, and scrolling of dense data, while programmatic selection remains visible. Dense data shows the fit view.

Categories are explicit labels. An empty list hides X-axis labels, and supplied categories must match every series. selectedValueFormatter and axisValueFormatter are independent and both receive Double values.

Maven namespace migration

HDCharts has moved to the organization-owned Maven group io.github.hdcharts. The personal identity io.github.dautovicharis no longer receives new artifacts.

Coordinates

Before (2.x)After (3.0.0)
Umbrellaio.github.dautovicharis:chartsio.github.hdcharts:charts
Modulesio.github.dautovicharis:charts-lineio.github.hdcharts:line
BOMio.github.dautovicharis:charts-bomio.github.hdcharts:bom

Packages

Each module now has its own Kotlin package, named after its artifact:

Before (2.x)After (3.0.0)
Shared models: io.github.dautovicharis.charts.model.*io.github.hdcharts.core.model.*
Shared styles: io.github.dautovicharis.charts.style.*io.github.hdcharts.core.style.*
LineChart, LiveLineChart, LineChartStyle, LineChartDefaultsio.github.hdcharts.line.*
PieChart, PieChartStyle, PieChartDefaultsio.github.hdcharts.pie.*
BarChartio.github.hdcharts.bar.*
HistogramChartio.github.hdcharts.histogram.*
StackedBarChart, StackedBarChartStyle, StackedBarChartDefaultsio.github.hdcharts.stackedbar.*
StackedAreaChart, StackedAreaChartStyle, StackedAreaChartDefaultsio.github.hdcharts.stackedarea.*
RadarChart, RadarChartStyle, RadarChartDefaultsio.github.hdcharts.radar.*

BarChartStyle, BarChartDefaults, HistogramChartStyle, and HistogramChartDefaults are shared by the bar and histogram modules, so they live in io.github.hdcharts.core.style.

Update both the dependency coordinates and the import statements in your project. Removing the old imports and letting the IDE re-import each symbol picks the new packages.

Before

import io.github.dautovicharis.charts.LineChart
import io.github.dautovicharis.charts.model.toChartData

commonMain.dependencies {
    implementation("io.github.dautovicharis:charts-line:2.4.0")
}

After

import io.github.hdcharts.core.model.toChartData
import io.github.hdcharts.line.LineChart

commonMain.dependencies {
    implementation("io.github.hdcharts:line:3.0.0")
}

Upgrade path from 2.x

Consumers staying on io.github.dautovicharis:charts-line:2.x do not need to change anything until they upgrade to 3.0.0. When a consumer that pins io.github.dautovicharis:charts-line:3.0.0 resolves the new version, Maven Central follows the relocation published under the old group and downloads the artifact from io.github.hdcharts:line:3.0.0 automatically. No manual intervention is required during the upgrade.

The relocation applies once at the 3.0.0 release. From 3.0.1 onward, the project only publishes under io.github.hdcharts. Pin the new coordinates directly when starting a new project.

Pie chart v3 migration

Use finite, nonnegative Double values when building the chart data.

Before

PieChart(
    dataSet = listOf(80f, 20f).toChartDataSet(
        title = "Progress",
        labels = listOf("Completed", "Remaining"),
    ),
    selectedSliceIndex = 0,
)

After

val selection = rememberChartSelection(initialIndex = 0)

PieChart(
    data = listOf(80.0, 20.0).toChartData(categories = listOf("Completed", "Remaining")),
    modifier = Modifier.fillMaxWidth(),
    title = "Progress",
    style = PieChartDefaults.style(
        donut = PieChartDefaults.donut(holePercentage = 50f),
    ),
    selection = selection,
)

Convert source values at the application boundary:

val data = sourceSlices.map { slice -> slice.value.toDouble() }
    .toChartData(categories = sourceSlices.map { slice -> slice.label })

The v3 API replaces the old ChartDataSet input with ChartData, the model every chart takes. A pie is one series whose values are the slices, so each category is a slice: its label names the slice in the legend and while it is selected. Categories are optional, as they are on every chart; a pie without them draws its slices with no legend.

Styles

Move customizations from the flat 2.x parameters to the grouped PieChartStyle sections. Slice, border, and legend values each get their own block:

// Before
PieChartDefaults.style(
    pieColor = Color.Blue,
    pieColors = listOf(Color.Red, Color.Green),
    pieAlpha = 0.8f,
    borderColor = Color.White,
    borderWidth = 2f,
    donutPercentage = 50f,
    legendVisible = false,
)

// After
PieChartDefaults.style(
    slices = PieChartDefaults.slices(baseColor = Color.Blue, alpha = 0.8f, colors = listOf(Color.Red, Color.Green)),
    border = PieChartDefaults.border(color = Color.White, width = 2.dp),
    donut = PieChartDefaults.donut(holePercentage = 50f),
    legend = PieChartDefaults.legend(visible = false),
)

pieColors becomes slices(colors = …), still one color per slice in slice order. Leave it empty and the chart generates a shade per slice from baseColor, the way it did in 2.x. A palette whose count does not match the slice count is reported as an error rather than drawn.

pieAlpha becomes slices(alpha = …). Sizes are Dp rather than the 2.x Float pixels, so divide pixel sizes by the screen density when migrating: borderWidth = 2f was 2.dp at 1x and 0.67.dp on a 3x screen.

The interim v3 snapshots replaced this with a PieSlice type that carried a color per slice. That type is gone; slice color is style, like every other chart.

Selection and interaction

Pass a ChartSelection directly to PieChart when selection must be controlled by the application:

val selection = rememberChartSelection()
PieChart(data = data, selection = selection)

Programmatic selection remains visible when interactionEnabled is false; in that mode, user taps and automatic deselection are disabled.

Radar v3 migration

Use shared ChartData with one ChartSeries per radar polygon. Categories are the shared axis labels.

Before

@Composable
private fun ShowRadar() {
    val categories = listOf(
        "Performance",
        "Reliability",
        "Usability",
        "Security",
        "Scalability",
        "Observability",
    )

    val dataSet = listOf(84f, 79f, 76f, 88f, 82f, 74f).toChartDataSet(
        title = "Platform Readiness Score",
        labels = categories,
    )

    RadarChart(
        dataSet = dataSet,
        selectedAxisIndex = 1,
    )
}

After

RadarChart(
    data = chartDataOf(
        categories = listOf("Performance", "Reliability", "Usability", "Security", "Scalability", "Observability"),
        ChartSeries("Platform Readiness Score", listOf(84.0, 79.0, 76.0, 88.0, 82.0, 74.0)),
    ),
    modifier = Modifier.fillMaxWidth(),
    title = "Platform Readiness Score",
    selection = rememberChartSelection(initialIndex = 1),
)

The old ChartDataSet, MultiChartDataSet, and selectedAxisIndex inputs are removed. Use the title and selection parameters instead. Use staticChartSelection(index) for a preset preview or screenshot.

Styles

Move customizations to the grouped RadarChartStyle sections such as grid, axes, polygon, points, and selection.

Axis labels are on by default: each category name sits at the end of its axis, and the web shrinks to leave room for them. Pass axes(labelVisible = false) for a web that fills the chart.

Data points are hidden by default, like on line charts; the selected axis still shows its points. Pass points(visible = true) to show every point.

The category legend is gone: the axis labels already name every category, and the legend only repeated them with a color that meant nothing. 2.x categoryColors, categoryPinsVisible, and categoryPinSize have no 3.x equivalent. The legend still names the series and shows each value while an axis is selected.

Selecting an axis dims the data points and labels of the other axes to 70% opacity. Polygons keep their full shape and color. Pass selection(unselectedAlpha = 1f) to keep every point solid.

Sizes are Dp and scale with screen density. The 2.x Float sizes were pixels, so divide them by the screen density when migrating. Point sizes are radii. The defaults are 1.dp grid and axis lines, 4 grid rings, 10.dp label padding, a 2.dp polygon line, and 4.dp data points.

Behavior

  • Use at least one series and three aligned axes. Values must be finite.
  • Categories are optional; when supplied, they must match every series.
  • When categories are provided, selecting an axis uses its category as the selected title and exposes the raw value for each series.
  • interactionEnabled = false disables drag gestures while programmatic selection remains visible.
  • Dragging selects an axis and the selection stays after the drag, so the values can be read. A tap clears it, so touch alone returns the chart to rest.
  • With more than one series, tapping a series outline focuses it and fades the others to 35% opacity. Where series overlap, each tap moves to the next one, and tapping empty space clears the focus. The focused series is held in the new seriesSelection parameter. interactionEnabled = false also disables these taps.

Shared v3 chart contracts

Use ChartData with Double values for the charts migrated to the v3 API.

Before

val data = listOf(24f, 18f).toChartData()

After

val sales = listOf(24.0, 18.0).toChartData(
    categories = listOf("Mon", "Tue"),
    seriesName = "Sales",
)

val comparison = listOf(
    "This year" to listOf(24.0, 18.0),
    "Last year" to listOf(20.0, 16.0),
).toChartData(categories = listOf("Mon", "Tue"))

Convert Int, Float, or string values in your application before creating ChartData or ChartSeries:

val data = sourceValues.map { it.toDouble() }.toChartData()

Handle invalid string values in the application; the library does not choose a parsing or missing-value policy for you.

Categories

Categories are explicit labels for the shared data index. An empty list means that the chart has no category labels. When provided, the category count must match every series. Index labels are not generated automatically.

Selection

Hoist selection with rememberChartSelection() when the application needs to observe or control it:

val selection = rememberChartSelection(
    onSelectionChanged = { index -> onPointSelected(index) },
)

Callbacks run only when the selected index changes. Use staticChartSelection(index) for an initial selection in a preview or screenshot.

Default colors

Series colors draw at full opacity. alpha defaults to 1f on bar, histogram, line, pie, stacked bar, and stacked area charts. For a softer look, pass alpha yourself. 2.x used 0.7f in light mode and 0.6f in dark mode:

BarChartDefaults.style(bars = BarChartDefaults.bars(alpha = 0.7f))

Grid, axis, label, and selection defaults use MaterialTheme.colorScheme roles. They follow your app's theme, not the system dark mode setting.

defaultChartAlpha() is removed. Pass an explicit alpha instead.

Style values out of range

Charts draw style values as close to what you pass as they can, instead of showing an error. Alphas are clamped to 0..1, sizes to at least 0.dp, and grid steps to at least 0. A NaN alpha draws at full opacity, and a NaN size uses the chart's default. The style object keeps the values you passed.

Data problems, such as misaligned series or a color count that does not match the series count, still show an error. Every chart words the same problem the same way, for example "At least 2 values are required." or "Series 1 is not aligned with the first series."

Formatting

ChartValueFormatter receives a Double. Value and axis formatters are independent, so a custom selected-value format does not change axis labels.

Stacked Area v3 migration

Use shared ChartData with one ChartSeries per contribution series. Categories identify the shared X positions.

Before

@Composable
private fun ShowStackedArea() {
    val items = listOf(
        "Free Plan" to listOf(620f, 650f, 690f, 720f, 760f, 800f),
        "Standard Plan" to listOf(240f, 260f, 285f, 310f, 340f, 365f),
        "Premium Plan" to listOf(90f, 95f, 105f, 118f, 130f, 142f),
    )

    val dataSet = items.toMultiChartDataSet(
        title = "Monthly Active Subscribers by Plan",
        categories = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"),
    )

    StackedAreaChart(
        dataSet = dataSet,
        selectedPointIndex = 1,
    )
}

After

StackedAreaChart(
    data = chartDataOf(
        categories = listOf("Jan", "Feb", "Mar", "Apr", "May", "Jun"),
        ChartSeries("Free Plan", listOf(620.0, 650.0, 690.0, 720.0, 760.0, 800.0)),
        ChartSeries("Standard Plan", listOf(240.0, 260.0, 285.0, 310.0, 340.0, 365.0)),
        ChartSeries("Premium Plan", listOf(90.0, 95.0, 105.0, 118.0, 130.0, 142.0)),
    ),
    modifier = Modifier.fillMaxWidth(),
    title = "Monthly Active Subscribers by Plan",
    selection = rememberChartSelection(initialIndex = 1),
)

The old MultiChartDataSet and selectedPointIndex inputs are removed. Use selection instead; it identifies one source X index shared by all series.

Styles and selection

Move customizations to the grouped StackedAreaChartStyle sections such as fill, axis, and selection. Series colors correspond to contribution series.

Stacked areas no longer draw boundary lines. Set the curve with StackedAreaChartDefaults.fill(bezier = true).

Selecting a point keeps a column around it at full color and dims the rest of the stack to 70% opacity. The selection line is drawn only above the stack. Pass selection(unselectedAlpha = 1f) to keep the whole stack at full color.

selectedValueFormatter formats the selected values, and axisValueFormatter formats the Y-axis labels.

Use staticChartSelection(index) for a preset preview or screenshot. Replacing data clears selection; resizing and density changes preserve it.

Behavior

  • Series must be aligned, contain at least two X positions, and use finite, nonnegative values.
  • Categories are optional; when supplied, they must match every series.
  • Stacking shows absolute totals rather than percentages.
  • interactionEnabled = false disables user controls while programmatic selection remains visible.

Stacked Bar v3 migration

Use shared ChartData with one ChartSeries per stack segment. Each category is one bar, and each series supplies one contribution for every bar.

Before

@Composable
private fun ShowStackedBar() {
    val items = listOf(
        "North America" to listOf(320f, 340f, 360f, 390f),
        "Europe" to listOf(210f, 230f, 245f, 260f),
        "Asia Pacific" to listOf(180f, 205f, 225f, 250f),
    )

    val dataSet = items.toMultiChartDataSet(
        title = "Quarterly Revenue by Region",
        prefix = "$",
        categories = listOf("Q1", "Q2", "Q3", "Q4"),
    )

    StackedBarChart(
        dataSet = dataSet,
        selectedBarIndex = 1,
    )
}

After

StackedBarChart(
    data = chartDataOf(
        categories = listOf("Q1", "Q2", "Q3", "Q4"),
        ChartSeries("North America", listOf(320.0, 340.0, 360.0, 390.0)),
        ChartSeries("Europe", listOf(210.0, 230.0, 245.0, 260.0)),
        ChartSeries("Asia Pacific", listOf(180.0, 205.0, 225.0, 250.0)),
    ),
    modifier = Modifier.fillMaxWidth(),
    title = "Quarterly Revenue by Region",
    selection = rememberChartSelection(initialIndex = 1),
)

The old row-oriented MultiChartDataSet input is removed. Transpose legacy rows when migrating: each row becomes one bar, its label becomes the category label, and each new series contains one segment column.

Styles and selection

Move customizations to the grouped StackedBarChartStyle sections such as segments, layout, axis, and selection. Segment colors correspond to series, not bars.

Use selection instead of selectedBarIndex. Selection applies to a whole bar, not an individual segment. Use staticChartSelection(index) for a preset preview or screenshot.

Selecting a bar dims the other bars to 70% opacity, and the selection line is drawn only above the selected bar. Pass selection(unselectedAlpha = 1f) to keep every bar solid.

The selection line width is Dp and scales with screen density. The default is 1.dp; in 2.x widths were pixels.

Replace the old dataset prefix with selectedValueFormatter, such as ChartValueFormatters.prefix("$"). axisValueFormatter formats the Y-axis labels.

Behavior

  • Series must be aligned, contain at least two bars, and use finite, nonnegative values.
  • Categories are optional; when supplied, they must match every series.
  • Stacks show absolute totals rather than percentages.
  • Replacing data clears selection. Resizing and density changes preserve it.
  • interactionEnabled = false disables user controls while programmatic selection remains visible.