diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 648410d1e4..bcecfefdb9 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -60,6 +60,7 @@ + diff --git a/app/src/main/kotlin/info/appdev/chartexample/TimeIntervalChartActivity.kt b/app/src/main/kotlin/info/appdev/chartexample/TimeIntervalChartActivity.kt new file mode 100644 index 0000000000..cb0c749f15 --- /dev/null +++ b/app/src/main/kotlin/info/appdev/chartexample/TimeIntervalChartActivity.kt @@ -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(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 + +} diff --git a/app/src/main/kotlin/info/appdev/chartexample/notimportant/MainActivity.kt b/app/src/main/kotlin/info/appdev/chartexample/notimportant/MainActivity.kt index 35572d42af..5f0ff4f1c7 100644 --- a/app/src/main/kotlin/info/appdev/chartexample/notimportant/MainActivity.kt +++ b/app/src/main/kotlin/info/appdev/chartexample/notimportant/MainActivity.kt @@ -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 @@ -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)) } } } diff --git a/app/src/main/res/layout/activity_time_interval_chart.xml b/app/src/main/res/layout/activity_time_interval_chart.xml new file mode 100644 index 0000000000..9f5a3e1c7c --- /dev/null +++ b/app/src/main/res/layout/activity_time_interval_chart.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/chartLib/src/main/kotlin/info/appdev/charting/charts/GanttChart.kt b/chartLib/src/main/kotlin/info/appdev/charting/charts/GanttChart.kt new file mode 100644 index 0000000000..1afe35bcf1 --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/charts/GanttChart.kt @@ -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.. 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.. = ArrayList() + + /** + * 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) { + 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() + } +} diff --git a/chartLib/src/main/kotlin/info/appdev/charting/data/GanttTask.kt b/chartLib/src/main/kotlin/info/appdev/charting/data/GanttTask.kt new file mode 100644 index 0000000000..9290551b7d --- /dev/null +++ b/chartLib/src/main/kotlin/info/appdev/charting/data/GanttTask.kt @@ -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 +} diff --git a/screenshotsToCompare9/StartTest_smokeTestStart-46-TimeIntervalChartActivity-Error.png b/screenshotsToCompare9/StartTest_smokeTestStart-46-TimeIntervalChartActivity-Error.png new file mode 100644 index 0000000000..65b04763bc Binary files /dev/null and b/screenshotsToCompare9/StartTest_smokeTestStart-46-TimeIntervalChartActivity-Error.png differ diff --git a/screenshotsToCompare9/StartTest_smokeTestStart-46-TimeIntervalChartActivity-Timeinterval-1SampleClick.png b/screenshotsToCompare9/StartTest_smokeTestStart-46-TimeIntervalChartActivity-Timeinterval-1SampleClick.png new file mode 100644 index 0000000000..65b04763bc Binary files /dev/null and b/screenshotsToCompare9/StartTest_smokeTestStart-46-TimeIntervalChartActivity-Timeinterval-1SampleClick.png differ