Gantt Chart
A Gantt chart: the tasks of a plan, a row each, every one a bar from the moment it starts to the moment it ends along one axis of time. It shows what runs when, what overlaps, and how far each task is.
import com.pantrixui.components.chart.gantt.PantrixGanttChart
- Tasks. The chart takes its tasks (
GanttTask) in the order of their rows; each task has a key, a name, and the moments it starts and ends. Moments arekotlin.time.Instant: no date library is needed. - Time. Time runs along the chart, from the start of the first task to the end of the last. Its axis is marked at round moments (day, week, month), as many as have room, and at each mark a line goes up through the rows. Moments are read by the device's clock and written in the theme's language, unless
time(ChartTime) says otherwise. - Progress. A task that gives
progresshas that much of its bar in full colour and the rest faint. - Milestone. A task that ends when it starts is a moment, and is drawn as a diamond.
- Today. Where
todayis given and lies in the chart's time, a line runs over the bars at it and "Today" is written on a line of its own over the rows. The chart has no clock of its own: the moment it is read at is the caller's to give. - Groups. Tasks that give a
groupgather under a row of the group's own: the group's bar runs from the start of its first task to the end of its last, with its ends pointing down; the tasks' names stand a little in from the group's. The group's name is the heading of its tasks' tooltip, and a screen reader says it too. - Rows. Tasks that give the same
rowshare a row, side by side in time, and the row is called by that name: a person's work in a row of their own. Tasks whose times overlap stand one under another in the row, and the row grows by a row's height for each. Within the row the pointer goes to the task it is over; the tasks share the row halfway between them. - Rings. Tasks that follow one another cannot close a ring: a task that follows a task that follows it is refused.
- Dependencies.
afterholds the keys of the tasks a task follows: from where each ends to where this one starts an arrow is drawn, turning at right angles; where the task starts too soon the arrow goes round below or above. A key the chart does not have, or the task's own, is refused. - Weekends.
weekends = trueshades the Saturdays and Sundays faint by the chart's clock; where a day is narrower than two pixels it shades none. - Names. The tasks' names stand in a column at the start of the rows, no wider than two fifths of the chart; a name that does not fit is cut short with three dots.
names = falsetakes the column away. - The task pointed at. The bar of the task whose row the pointer or the finger is in is ringed, and the tooltip writes its name, the moments it starts and ends and, where it is said, how much of it is done. With the keyboard the arrows up and down go from task to task.
- Size. Given no height, the chart is as tall as its rows need, group headers among them: thirty-two dp a row, and one more over them where "Today" is written.
- Export.
exportputs a button over the chart: the chart is made, as it is on the screen and without the part that is pointed at or its tooltip, into a PNG, an SVG or a PDF file and handed to the app. - Zoom. With
zooma part of the time is shown as wide as the chart: a wheel (with Ctrl or Command), two fingers, the plus and minus keys and the buttons look closer; a drag and the buttons move along the time. - Automation. The chart is tagged
Gantt_<tagKey>and a taskGantt_<tagKey>.Task_<number>. A press on a task is reported as aselectevent with the task's key.
Examples
The plan of a release
The plan of a release: four tasks and two milestones. The tasks that are finished are in full colour; of those under way the part that is done is in full colour and the rest faint. The upright line shows today (today): what is to its left is past.
Move the pointer over a row, or touch the row: the bar is ringed and the tooltip writes the task's start, its end and how far it is. The days are days of UTC, so the chart is read by UTC too (the offset of ChartTime).
PantrixGanttChart(
tasks = RELEASE_PLAN,
time = ChartTime(offset = { Duration.ZERO }),
today = dayAt("04-14"),
description = "The plan of the spring release: from research to release",
tagKey = "plain"
)
private val RELEASE_PLAN = listOf(
GanttTask("research", "Research", start = dayAt("03-02"), end = dayAt("03-14"), progress = 1f),
GanttTask("design", "Design", start = dayAt("03-09"), end = dayAt("03-28"), progress = 1f),
GanttTask("build", "Development", start = dayAt("03-23"), end = dayAt("04-25"), progress = 0.55f),
GanttTask("test", "Testing", start = dayAt("04-13"), end = dayAt("05-02"), progress = 0.1f),
GanttTask("candidate", "Release candidate", start = dayAt("04-27"), end = dayAt("04-27")),
GanttTask("release", "Release", start = dayAt("05-04"), end = dayAt("05-04"))
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
Your own colours and rows with no names
Each task can carry a colour of its own (the color field of GanttTask): here the design work is in one colour, the software work in another and the milestones in a third. Turn the switch: the names' column goes away (names = false) and the bars take the whole width; the names stay in the tooltip and for a screen reader.
var named by remember { mutableStateOf(true) }
val theme = PantrixTheme.colors
val design = setOf("research", "design")
PantrixSwitchButton(
checked = named,
onCheckedChange = { named = it },
label = "Task names",
tagKey = "gantt_names"
)
PantrixGanttChart(
tasks = RELEASE_PLAN.map { task ->
val color = when {
task.start == task.end -> theme.warning.color
task.key in design -> theme.info.color
else -> theme.success.color
}
GanttTask(task.key, task.name, task.start, task.end, task.progress, color)
},
time = ChartTime(offset = { Duration.ZERO }),
names = named,
description = "The plan of the spring release, in the teams' colours",
tagKey = "own_colors"
)
private val RELEASE_PLAN = listOf(
GanttTask("research", "Research", start = dayAt("03-02"), end = dayAt("03-14"), progress = 1f),
GanttTask("design", "Design", start = dayAt("03-09"), end = dayAt("03-28"), progress = 1f),
GanttTask("build", "Development", start = dayAt("03-23"), end = dayAt("04-25"), progress = 0.55f),
GanttTask("test", "Testing", start = dayAt("04-13"), end = dayAt("05-02"), progress = 0.1f),
GanttTask("candidate", "Release candidate", start = dayAt("04-27"), end = dayAt("04-27")),
GanttTask("release", "Release", start = dayAt("05-04"), end = dayAt("05-04"))
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
The hours of a day
The hours of a day: the steps of a release day. Since the moments are within one day the axis is marked at hours, and the tooltip writes the start and the end with their time of day. On a narrow screen the second task's name has no room in its column and is cut short with three dots; the tooltip writes it whole.
PantrixGanttChart(
tasks = RELEASE_DAY,
time = ChartTime(offset = { Duration.ZERO }),
today = hourAt("11:20"),
description = "The steps of the release day, hour by hour",
tagKey = "hours"
)
private val RELEASE_DAY = listOf(
GanttTask("freeze", "Code freeze", start = hourAt("08:00"), end = hourAt("09:00"), progress = 1f),
GanttTask(
key = "checks",
name = "Final checks, approvals and release notes",
start = hourAt("09:00"),
end = hourAt("11:30"),
progress = 0.9f
),
GanttTask("rollout", "Staged rollout", start = hourAt("11:30"), end = hourAt("15:00")),
GanttTask("watch", "Monitoring", start = hourAt("11:30"), end = hourAt("18:00")),
GanttTask("done", "Done", start = hourAt("18:00"), end = hourAt("18:00"))
)
private fun hourAt(time: String): Instant = Instant.parse("2026-05-04T$time:00Z")
Pressing a task
A press is handed the task: its key is in its series, its name in its name, how many days it takes in its value, and what the chart writes in its text. Press a row: the line below writes what was pressed.
var pressed by remember { mutableStateOf<String?>(null) }
val strings = PantrixTheme.strings
PantrixGanttChart(
tasks = RELEASE_PLAN,
time = ChartTime(offset = { Duration.ZERO }),
onSelect = { pressed = "${it.name}: ${strings.decimal(it.value)} days" },
description = "The plan of the spring release",
tagKey = "press"
)
PantrixText(text = pressed ?: "Press a task")
private val RELEASE_PLAN = listOf(
GanttTask("research", "Research", start = dayAt("03-02"), end = dayAt("03-14"), progress = 1f),
GanttTask("design", "Design", start = dayAt("03-09"), end = dayAt("03-28"), progress = 1f),
GanttTask("build", "Development", start = dayAt("03-23"), end = dayAt("04-25"), progress = 0.55f),
GanttTask("test", "Testing", start = dayAt("04-13"), end = dayAt("05-02"), progress = 0.1f),
GanttTask("candidate", "Release candidate", start = dayAt("04-27"), end = dayAt("04-27")),
GanttTask("release", "Release", start = dayAt("05-04"), end = dayAt("05-04"))
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
Groups and dependencies
The tasks in their teams' groups (group): each group has a row of its own and a bar that sums its tasks up from start to end. The arrows show which task follows which (after): Development starts once Design ends. Testing starts before Development ends, so its arrow goes round below.
PantrixGanttChart(
tasks = GROUPED_PLAN,
time = ChartTime(offset = { Duration.ZERO }),
today = dayAt("04-14"),
description = "The plan of the spring release, by team",
tagKey = "groups"
)
private val GROUPED_PLAN = listOf(
GanttTask("research", "Research", dayAt("03-02"), dayAt("03-14"), progress = 1f, group = "Product"),
GanttTask(
key = "design",
name = "Design",
start = dayAt("03-16"),
end = dayAt("03-28"),
progress = 1f,
after = listOf("research"),
group = "Product"
),
GanttTask(
key = "build",
name = "Development",
start = dayAt("03-30"),
end = dayAt("04-25"),
progress = 0.55f,
after = listOf("design"),
group = "Software"
),
GanttTask(
key = "test",
name = "Testing",
start = dayAt("04-13"),
end = dayAt("05-02"),
progress = 0.1f,
after = listOf("build"),
group = "Software"
),
GanttTask("release", "Release", dayAt("05-04"), dayAt("05-04"), after = listOf("test"))
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
People's rows and weekends
People's rows (row): each person's tasks in a row of their own, side by side. Ada's hotfix falls on the same days as the profile, so it stands under it in her row. The weekends are faint (weekends). Move the pointer along a row: the task it is over is shown.
PantrixGanttChart(
tasks = SPRINT,
time = ChartTime(offset = { Duration.ZERO }),
description = "Who does what in a two-week sprint",
tagKey = "rows",
weekends = true
)
private val SPRINT = listOf(
GanttTask("login", "Sign-in screen", dayAt("03-02"), dayAt("03-05"), progress = 1f, row = "Ada"),
GanttTask("profile", "Profile", dayAt("03-05"), dayAt("03-11"), progress = 0.5f, row = "Ada"),
GanttTask("hotfix", "Hotfix", dayAt("03-06"), dayAt("03-10"), progress = 0.3f, row = "Ada"),
GanttTask("api", "API", dayAt("03-02"), dayAt("03-07"), progress = 1f, row = "Lin"),
GanttTask("sync", "Sync", dayAt("03-09"), dayAt("03-14"), after = listOf("api"), row = "Lin"),
GanttTask("review", "Review", dayAt("03-11"), dayAt("03-14"), after = listOf("profile"), row = "Can"),
GanttTask("demo", "Demo", dayAt("03-16"), dayAt("03-16"), after = listOf("review", "sync"), row = "Can")
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
Export
export = ChartExport(...) puts an export button over the chart: press it and choose the kind (PNG, SVG or PDF). The chart makes the file and hands it to onExport; below, its name and size show, and a PNG's picture, and "Save the file" hands it over in the platform's way.
The file has the rows, the bars, the marks of time and the line of today with its name.
var file by remember { mutableStateOf<ChartExportFile?>(null) }
PantrixGanttChart(
tasks = RELEASE_PLAN,
time = ChartTime(offset = { Duration.ZERO }),
today = dayAt("04-14"),
description = "The plan of the spring release: from research to release",
tagKey = "exported",
export = ChartExport(fileName = "spring-release") { file = it }
)
ExportedFile(file, tagKey = "exported")
private val RELEASE_PLAN = listOf(
GanttTask("research", "Research", start = dayAt("03-02"), end = dayAt("03-14"), progress = 1f),
GanttTask("design", "Design", start = dayAt("03-09"), end = dayAt("03-28"), progress = 1f),
GanttTask("build", "Development", start = dayAt("03-23"), end = dayAt("04-25"), progress = 0.55f),
GanttTask("test", "Testing", start = dayAt("04-13"), end = dayAt("05-02"), progress = 0.1f),
GanttTask("candidate", "Release candidate", start = dayAt("04-27"), end = dayAt("04-27")),
GanttTask("release", "Release", start = dayAt("05-04"), end = dayAt("05-04"))
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
Looking closer at the time
zoom = ChartZoom() lets the reader look closer at the time: a wheel with Ctrl or Command, two fingers, plus and minus on the chart, and the buttons over it show a part of the time as wide as the chart. A drag of the mouse and the buttons move along it; the task the arrows go to comes into view.
What lies past the part in view is cut off at the time's two ends, and the names stay. The part is kept in the state (state.window): this chart opens on the first half of the plan, and nought or the last button shows all of it again.
val state = rememberChartState()
// The chart opens on the first half of the plan.
LaunchedEffect(state) { state.zoomTo(start = 0f, end = 0.5f) }
PantrixGanttChart(
tasks = RELEASE_PLAN,
time = ChartTime(offset = { Duration.ZERO }),
today = dayAt("04-14"),
state = state,
description = "The plan of the spring release: from research to release",
tagKey = "zoomed",
zoom = ChartZoom()
)
private val RELEASE_PLAN = listOf(
GanttTask("research", "Research", start = dayAt("03-02"), end = dayAt("03-14"), progress = 1f),
GanttTask("design", "Design", start = dayAt("03-09"), end = dayAt("03-28"), progress = 1f),
GanttTask("build", "Development", start = dayAt("03-23"), end = dayAt("04-25"), progress = 0.55f),
GanttTask("test", "Testing", start = dayAt("04-13"), end = dayAt("05-02"), progress = 0.1f),
GanttTask("candidate", "Release candidate", start = dayAt("04-27"), end = dayAt("04-27")),
GanttTask("release", "Release", start = dayAt("05-04"), end = dayAt("05-04"))
)
private fun dayAt(day: String): Instant = Instant.parse("2026-${day}T00:00:00Z")
API
tasks
A Gantt chart: the tasks of a plan, a row each, every one a bar from the moment it starts to the moment it ends along one axis of time. It shows what runs when, what overlaps, and how far each task is.
| Parameter | Type | Default | What it is |
|---|---|---|---|
tasks | List<GanttTask> | the tasks, in the order of their rows. Their keys tell them apart, and two with one key are refused. | |
modifier | Modifier | Modifier | the chart's modifier. A height here is the chart's. |
time | ChartTime? | null | how the chart's moments are written and by which clock. Left out, in the theme's language and by the screen's clock (LocalPantrixClock). |
today | Instant? | null | the moment the chart is read at: a line is drawn at it where it lies between the start of the first task and the end of the last. null draws none. |
names | Boolean | true | whether the tasks' names are written beside their rows. Without them the bars have the whole width, and the names stay in the tooltip and for a screen reader. |
tooltip | ChartTooltip? | ChartTooltip() | what is shown for the task that is pointed at: its group and its row as the heading, where it has them, and under it its name, when it starts and ends, and how much of it is done. A task has no place to share and no line through it: shared, crosshair and format are not read. null shows none. |
empty | (@Composable () -> Unit)? | null | what the chart shows while it has no value to draw, in the place of the theme's words for it ("No data"), which a screen reader is then not told either. null writes the words. |
state | ChartState | rememberChartState() | what the reader points at: the chart's own where none is given. |
description | String? | null | what the chart shows, in a few words, for a screen reader: the first thing it says of the chart. Left out, the chart is announced by the names of its tasks. |
onSelect | ((ChartPoint) -> Unit)? | null | called with the task that is pressed: its key is its series and its x, its name its name and its label, its value how many days it takes, and its text when it starts and ends as the chart writes it. |
style | ChartStyle? | null | this chart's own geometry: the fields set here, with the theme's for the rest. A bar's corners are as round as barRadius says, and the lines at the marks of time as wide as gridWidth. |
colors | ChartColors? | null | this chart's own colours, where they are not the theme's. |
tagKey | String? | null | the chart's automation key: the chart is Gantt_<tagKey>, its rows together Gantt_<tagKey>.Plot, a task Gantt_<tagKey>.Task_<number>, counted from one, and the tooltip Gantt_<tagKey>.Tooltip. A press on a task is reported as a Gantt select event with the task's key. Nothing of its moments is reported. |
weekends | Boolean | false | whether the Saturdays and Sundays of the chart's time, by its clock, are shaded. None are where a day is narrower than two pixels. |
export | ChartExport? | null | what lets the reader take the chart out of the app as a PNG, an SVG or a PDF: a button over the chart makes the file of it as it is and hands it over (see ChartExport). null, the chart has no such button. |
zoom | ChartZoom? | null | what lets the reader look closer at a part of the chart's time, as a chart with axes is looked at closer along its places (see ChartZoom): a wheel with Ctrl or Command, two fingers, plus and minus on the chart, and its buttons bring a part of the time into view, as wide as the chart; a drag of the mouse and the buttons move along it, and the arrows bring the task they go to into view. What lies past the part in view is cut off at the time's two ends; the names stay. The part is kept in state, as its window. A Gantt chart has no navigator: its navigator is not read, nor is its fitValues. null, the chart shows all its time. |
ChartStyle
A chart's geometry: how tall it is where nothing says, how thick its lines and how large its marks are, and the room between its parts; and whether its fills have textures besides their colours.
| Parameter | Type | Default | What it is |
|---|---|---|---|
height | Dp | Dp.Unspecified | How tall a chart's plot is, with its axes, where the chart's modifier gives it no height. A legend above or under the plot adds its own. |
lineWidth | Dp | Dp.Unspecified | The width of a series' line. |
markerSize | Dp | Dp.Unspecified | The width and height of the mark on a point. |
selectedMarkerSize | Dp | Dp.Unspecified | The same of the mark on the point that is pointed at. |
gridWidth | Dp | Dp.Unspecified | The width of the grid's lines, of the lines along the axes and of the line that marks the place pointed at. Zero draws none. |
labelGap | Dp | Dp.Unspecified | Between the plot and what is written beside it and under it, and between that and an axis' title. |
tickSpacing | Dp | Dp.Unspecified | The least room between two values marked along the axis of values: what decides how many are marked where the axis does not say. |
legendGap | Dp | Dp.Unspecified | Between the legend and the chart it stands beside. |
legendItemGap | Dp | Dp.Unspecified | Between one entry of the legend and the next. |
swatchSize | Dp | Dp.Unspecified | How wide the sample of a series is in the legend and in the tooltip: a short run of its line, or a patch of its fill. |
swatchGap | Dp | Dp.Unspecified | Between that sample and the name after it. |
tooltipShape | ShapeSpec | ShapeSpec.Unspecified | The tooltip's corners: a step of the brand's shape scale, or a fixed shape. |
tooltipHorizontalPadding | Dp | Dp.Unspecified | Between the tooltip's sides and what it holds. |
tooltipVerticalPadding | Dp | Dp.Unspecified | Between its top and bottom and what it holds, and between its heading and rows. |
tooltipGap | Dp | Dp.Unspecified | Between the place pointed at and the tooltip beside it. |
tooltipElevation | Dp | Dp.Unspecified | The tooltip's shadow. |
barGap | Dp | Dp.Unspecified | Between two bars that stand side by side at a place. Bars too thin for it stand closer. |
barMaxWidth | Dp | Dp.Unspecified | The widest a bar is, however much room its place has: a chart of three places is not three slabs. |
barRadius | Dp | Dp.Unspecified | How round the corners of a bar's end are, the end away from nought. |
sliceGap | Dp | Dp.Unspecified | Between two slices of a pie that lie side by side, the same from the pie's middle to its rim. |
selectedSliceOffset | Dp | Dp.Unspecified | How far the slice of a pie that is pointed at stands out past the pie's rim. The pie keeps this much room about itself, so that the slice has where to stand. |
gaugeWidth | Dp | Dp.Unspecified | How thick the arc of a gauge is. An arc is no thicker than half its radius. |
meterHeight | Dp | Dp.Unspecified | How tall the bar of a meter group is. |
trackerHeight | Dp | Dp.Unspecified | How tall the blocks of a status tracker are. |
bubbleMinSize | Dp | Dp.Unspecified | How far across the smallest bubble of a scatter chart is: a point whose size is nothing, or too little to be seen as an area, is still a point. |
bubbleMaxSize | Dp | Dp.Unspecified | How far across its largest bubble is: the one whose size is the most of the chart's. |
navigatorHeight | Dp | Dp.Unspecified | How tall the strip under a chart's plot is, on which the part of the chart that is in view is marked and moved, in a chart whose zoom has one: how wide, beside a plot whose places go down its side. |
barBandShare | Float | Float.NaN | How much of a place's band its bars take, more than nought and one at the most: the rest is the room between one place's bars and the next's. Bars are no wider than barMaxWidth all the same. |
markLineWidth | Dp | Dp.Unspecified | The width of a mark's line: a target, a limit, a day. Zero draws none, and the mark's words are written all the same. |
markBandAlpha | Float | Float.NaN | How much of its colour a mark's band is drawn in, from nought to one: a band lies behind the series, and is a tint. |
patterns | Boolean? | null | Whether the fills of a chart's series, slices and tiles are drawn with textures besides their colours, and its lines broken in turn (ChartPattern): what tells them apart for a reader who cannot tell the colours apart. Left out, as the system is set: on where it is set to tell things apart without colour, iOS's "Differentiate Without Color" and Android's colour correction, and off otherwise. |
nodeWidth | Dp | Dp.Unspecified | How wide a node of a sankey is. |
nodeGap | Dp | Dp.Unspecified | Between two nodes of a column of a sankey. |
ChartColors
What a chart is painted with, for a chart that does not take the theme's colours: a colour that is set is used, and one that is left out stays the theme's.
| Parameter | Type | Default | What it is |
|---|---|---|---|
series | List<Color> | emptyList() | The colours the series are drawn in, in the order they are handed out, for the series that name none of their own. Empty, they are the theme's chart colours. |
grid | Color | Color.Unspecified | The lines drawn across the chart at the marks of an axis. The theme's divider. |
axis | Color | Color.Unspecified | The line along an axis. The theme's border. |
label | Color | Color.Unspecified | What is written beside the axes: the values, the places and the axes' titles, and the names in the legend. The theme's textSecondary. |
crosshair | Color | Color.Unspecified | The line that marks the place pointed at. The theme's borderHover: stronger than the grid it crosses, and quieter than the series it stands behind. |
tooltipContainer | Color | Color.Unspecified | The tooltip's fill. The theme's surface. |
tooltipBorder | Color | Color.Unspecified | The tooltip's edge. The theme's border. |
tooltipText | Color | Color.Unspecified | The tooltip's heading and its values. The theme's textPrimary. |
tooltipLabel | Color | Color.Unspecified | The names of the series in the tooltip. The theme's textSecondary. |
rising | Color | Color.Unspecified | What a value that goes up is drawn in: a step of a waterfall that adds. The theme's success. |
falling | Color | Color.Unspecified | What a value that goes down is drawn in: a step of a waterfall that takes away. The theme's danger. |
Accessibility
- Screen reader. First the chart's description, then where the today line is drawn "Today" and its day, then each task in the tasks' order: its group and row where it has them, its name, its start, its end and its progress.