Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
<activity android:name="info.appdev.chartexample.HalfPieChartActivity" />
<activity android:name="info.appdev.chartexample.SpecificPositionsLineChartActivity" />
<activity android:name="info.appdev.chartexample.TimeLineActivity" />
<activity android:name="info.appdev.chartexample.TimeIntervalChartActivity" />
</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package info.appdev.chartexample

import android.graphics.Color
import android.os.Bundle
import info.appdev.chartexample.notimportant.DemoBase
import info.appdev.charting.charts.GanttChart
import info.appdev.charting.data.EntryFloat
import info.appdev.charting.data.GanttChartData
import info.appdev.charting.data.GanttTask
import info.appdev.charting.highlight.Highlight
import info.appdev.charting.listener.OnChartValueSelectedListener

/**
* Demo activity showing Gantt-style timeline visualization.
* Each horizontal bar represents a task with start time and duration.
*/
class TimeIntervalChartActivity : DemoBase(), OnChartValueSelectedListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_time_interval_chart)

val chart = findViewById<GanttChart>(R.id.chart)

// Create Gantt chart data
val ganttData = GanttChartData()

// Add sample project tasks
ganttData.addTask(GanttTask("Design", 0f, 50f, Color.rgb(255, 107, 107))) // Red: 0-50
ganttData.addTask(GanttTask("Development", 40f, 100f, Color.rgb(66, 165, 245))) // Blue: 40-140
ganttData.addTask(GanttTask("Testing", 120f, 40f, Color.rgb(76, 175, 80))) // Green: 120-160
ganttData.addTask(GanttTask("Launch", 150f, 20f, Color.rgb(255, 193, 7))) // Yellow: 150-170

// Set data and render
chart.setData(ganttData)
}

override fun saveToGallery() = Unit

override fun onValueSelected(entryFloat: EntryFloat, highlight: Highlight) = Unit

override fun onNothingSelected() = Unit

}
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import info.appdev.chartexample.ScrollViewActivity
import info.appdev.chartexample.SpecificPositionsLineChartActivity
import info.appdev.chartexample.StackedBarActivity
import info.appdev.chartexample.StackedBarActivityNegative
import info.appdev.chartexample.TimeIntervalChartActivity
import info.appdev.chartexample.TimeLineActivity
import info.appdev.chartexample.compose.HorizontalBarComposeActivity
import info.appdev.chartexample.compose.HorizontalBarFullComposeActivity
Expand Down Expand Up @@ -219,6 +220,7 @@ class MainActivity : ComponentActivity() {
add(ContentItem("Demonstrate and fix issues"))
add(ContentItem("Gradient", "Show a gradient edge case", GradientActivity::class.java))
add(ContentItem("Timeline", "Show a time line with Unix timestamp", TimeLineActivity::class.java))
add(ContentItem("Timeinterval", "Grantt chart", TimeIntervalChartActivity::class.java))
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/res/layout/activity_time_interval_chart.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">

<info.appdev.charting.charts.GanttChart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="match_parent" />

</FrameLayout>
185 changes: 185 additions & 0 deletions chartLib/src/main/kotlin/info/appdev/charting/charts/GanttChart.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package info.appdev.charting.charts

import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import info.appdev.charting.data.GanttChartData
import java.util.Locale

class GanttChart : View {
private var data: GanttChartData? = null
private var taskPaint: Paint? = null
private var gridPaint: Paint? = null
private var textPaint: Paint? = null

private var chartLeft = 0f
private var chartTop = 0f
private var chartRight = 0f
private var chartBottom = 0f
private val padding = 16f
private val labelTextSize = 24f
private val gridLinesMin = 2
private val gridLinesMax = 10

constructor(context: Context?) : super(context) {
init()
}

constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs) {
init()
}

constructor(context: Context?, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
init()
}

private fun init() {
taskPaint = Paint().apply {
isAntiAlias = true
}
gridPaint = Paint().apply {
color = -0x333334
strokeWidth = 1f
}
textPaint = Paint().apply {
color = -0x99999a
textSize = 28f
isAntiAlias = true
}
}

fun setData(data: GanttChartData?) {
this.data = data
invalidate()
}

override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)

if (data == null || data!!.taskCount == 0) {
return
}

calculateDimensions()
drawGrid(canvas)
drawTasks(canvas)
}

private fun calculateDimensions() {
val labelMeasurePaint = Paint().apply {
textSize = labelTextSize
isAntiAlias = true
}
var maxLabelWidth = 0f
if (data != null) {
for (i in 0..<data!!.taskCount) {
val w = labelMeasurePaint.measureText(data!!.getTask(i).name ?: "")
if (w > maxLabelWidth) maxLabelWidth = w
}
}
chartLeft = maxLabelWidth + padding * 3
chartTop = padding + 30
chartRight = width - padding
chartBottom = height - padding - 30
}

private val taskHeight: Float
// Dynamically calculate task height based on available space
get() {
if (data == null || data!!.taskCount == 0) {
return 40f
}
val availableHeight = chartBottom - chartTop
val taskCount = data!!.taskCount
// 50% of slot for bar, 50% for gap
return (availableHeight / taskCount) * 0.5f
}

private val taskSpacing: Float
get() {
if (data == null || data!!.taskCount == 0) {
return 12f
}
val availableHeight = chartBottom - chartTop
val taskCount = data!!.taskCount
return (availableHeight / taskCount) * 0.5f
}

private fun drawGrid(canvas: Canvas) {
val minTime = data!!.minTime
val maxTime = data!!.maxTime
var timeRange = maxTime - minTime
if (timeRange == 0f) {
timeRange = 100f
}

val timeLabelPaint = Paint().apply {
color = -0x99999a
textSize = 22f
isAntiAlias = true
textAlign = Paint.Align.CENTER
}

// Calculate how many grid lines fit without overlapping labels
val sampleLabel = String.format(Locale.getDefault(), "%.0f", maxTime)
val labelWidth = timeLabelPaint.measureText(sampleLabel) + 8f
val chartWidth = chartRight - chartLeft
val maxGridLines = (chartWidth / labelWidth).toInt().coerceIn(gridLinesMin, gridLinesMax)

for (i in 0..maxGridLines) {
val x = chartLeft + (i / maxGridLines.toFloat()) * chartWidth
canvas.drawLine(x, chartTop, x, chartBottom, gridPaint!!)

val time = minTime + (i / maxGridLines.toFloat()) * timeRange
canvas.drawText(String.format(Locale.getDefault(), "%.0f", time), x, chartBottom + 30, timeLabelPaint)
}
}

private fun drawTasks(canvas: Canvas) {
val minTime = data!!.minTime
val maxTime = data!!.maxTime
var timeRange = maxTime - minTime
if (timeRange == 0f) {
timeRange = 100f
}

val taskHeight = this.taskHeight
val taskSpacing = this.taskSpacing
val slotHeight = taskHeight + taskSpacing

val labelPaint = Paint()
labelPaint.color = -0xcccccd
labelPaint.textSize = labelTextSize
labelPaint.isAntiAlias = true
labelPaint.textAlign = Paint.Align.RIGHT

val borderPaint = Paint()
borderPaint.color = -0x666667
borderPaint.strokeWidth = 2f
borderPaint.style = Paint.Style.STROKE

for (i in 0..<data!!.taskCount) {
val task = data!!.getTask(i)

val taskY = chartTop + i * slotHeight
val startX = chartLeft + ((task.startTime - minTime) / timeRange) * (chartRight - chartLeft)
var endX = chartLeft + ((task.endTime - minTime) / timeRange) * (chartRight - chartLeft)

if (endX - startX < 10) {
endX = startX + 10
}

// Center label vertically in the slot
val labelY = taskY + (taskHeight / 2) + 8
canvas.drawText(task.name!!, chartLeft - padding, labelY, labelPaint)

val rect = RectF(startX, taskY, endX, taskY + taskHeight)
taskPaint!!.color = task.color
canvas.drawRect(rect, taskPaint!!)
canvas.drawRect(rect, borderPaint)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package info.appdev.charting.data

import kotlin.math.max
import kotlin.math.min

/**
* Data container for Gantt chart.
* Manages a list of tasks and provides convenient access methods.
*/
class GanttChartData {
/**
* Get all tasks.
*
* @return List of all tasks
*/
val tasks: MutableList<GanttTask> = ArrayList<GanttTask>()

/**
* Add a task to the Gantt chart.
*
* @param task The task to add
*/
fun addTask(task: GanttTask?) {
tasks.add(task!!)
}

/**
* Add multiple tasks to the Gantt chart.
*
* @param taskList List of tasks to add
*/
fun addTasks(taskList: MutableList<GanttTask>) {
tasks.addAll(taskList)
}

/**
* Get a specific task by index.
*
* @param index Task index
* @return The task at the given index
*/
fun getTask(index: Int): GanttTask {
return tasks[index]
}

/**
* Get the number of tasks.
*
* @return Number of tasks in the chart
*/
val taskCount: Int
get() = tasks.size

/**
* Get the earliest start time across all tasks.
*
* @return Minimum start time
*/
val minTime: Float
get() {
if (tasks.isEmpty()) return 0f
var min = Float.MAX_VALUE
for (task in tasks) {
min = min(min, task.startTime)
}
return min
}

/**
* Get the latest end time across all tasks.
*
* @return Maximum end time
*/
val maxTime: Float
get() {
if (tasks.isEmpty()) return 100f
var max = 0f
for (task in tasks) {
max = max(max, task.endTime)
}
return max
}

/**
* Clear all tasks.
*/
fun clearTasks() {
tasks.clear()
}
}
15 changes: 15 additions & 0 deletions chartLib/src/main/kotlin/info/appdev/charting/data/GanttTask.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package info.appdev.charting.data

/**
* Represents a single task in a Gantt chart.
* Each task has a name, start time, duration, and display color.
*
* @param name Task name/label
* @param startTime When the task starts
* @param duration How long the task lasts
* @param color Display color (Android color int)
*/
class GanttTask(val name: String?, val startTime: Float, val duration: Float, val color: Int) {
val endTime: Float
get() = startTime + duration
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading