core: Implement bar chart

pull/524/head
Alinson S. Xavier 6 years ago
parent 3e2cf48223
commit bfea4b024a

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

@ -46,10 +46,10 @@ data class LocalDate(val daysSince2000: Int) {
var monthCache = -1
var dayCache = -1
init {
if (daysSince2000 < 0)
throw IllegalArgumentException("$daysSince2000 < 0")
}
// init {
// if (daysSince2000 < 0)
// throw IllegalArgumentException("$daysSince2000 < 0")
// }
constructor(year: Int, month: Int, day: Int) :
this(daysSince2000(year, month, day))

@ -0,0 +1,164 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.platform.gui.*
import org.isoron.platform.time.*
import kotlin.math.*
class BarChart(var theme: Theme,
var dateFormatter: LocalDateFormatter) : Component {
// Data
var series = mutableListOf<List<Double>>()
var colors = mutableListOf<Color>()
var axis = listOf<LocalDate>()
// Style
var paddingTop = 20.0
var paddingLeft = 5.0
var paddingRight = 5.0
var footerHeight = 40.0
var barGroupMargin = 4.0
var barMargin = 4.0
var barWidth = 20.0
var nGridlines = 6
var backgroundColor = theme.cardBackgroundColor
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()
val height = canvas.getHeight()
val n = series.size
val barGroupWidth = 2 * barGroupMargin + n * (barWidth + 2 * barMargin)
val safeWidth = width - paddingLeft - paddingRight
val nColumns = floor((safeWidth) / barGroupWidth).toInt()
val marginLeft = (safeWidth - nColumns * barGroupWidth) / 2
val maxBarHeight = height - footerHeight - paddingTop
var maxValue = series.map { it.max()!! }.max()!!
maxValue = max(maxValue, 1.0)
canvas.setColor(backgroundColor)
canvas.fillRect(0.0, 0.0, width, height)
fun barGroupOffset(c: Int) = marginLeft + paddingLeft +
(nColumns - c - 1) * barGroupWidth
fun barOffset(c: Int, s: Int) = barGroupOffset(c) +
barGroupMargin +
s * (barWidth + 2 * barMargin) +
barMargin
fun drawColumn(s: Int, c: Int) {
val value = if (c < series[s].size) series[s][c] else 0.0
val perc = value / maxValue
val barColorPerc = if (n > 1) 1.0 else round(perc / 0.20) * 0.20
val barColor = theme.lowContrastTextColor.blendWith(colors[s],
barColorPerc)
val barHeight = round(maxBarHeight * perc)
val x = barOffset(c, s)
val y = height - footerHeight - barHeight
canvas.setColor(barColor)
val r = round(barWidth * 0.33)
canvas.fillRect(x, y + r, barWidth, barHeight - r)
canvas.fillRect(x + r, y, barWidth - 2 * r, r)
canvas.fillCircle(x + r, y + r, r)
canvas.fillCircle(x + barWidth - r, y + r, r)
canvas.setFontSize(theme.smallTextSize)
canvas.setTextAlign(TextAlign.CENTER)
canvas.setColor(backgroundColor)
canvas.fillRect(x - barMargin,
y - theme.smallTextSize * 1.25,
barWidth + 2 * barMargin,
theme.smallTextSize * 1.0)
canvas.setColor(theme.mediumContrastTextColor)
canvas.drawText(value.toShortString(),
x + barWidth / 2,
y - theme.smallTextSize * 0.80)
}
fun drawSeries(s: Int) {
for (c in 0 until nColumns) drawColumn(s, c)
}
fun drawMajorGrid() {
canvas.setStrokeWidth(1.0)
if (n > 1) {
canvas.setColor(backgroundColor.blendWith(
theme.lowContrastTextColor,
0.5))
for (c in 0 until nColumns - 1) {
val x = barGroupOffset(c)
canvas.drawLine(x, paddingTop, x, paddingTop + maxBarHeight)
}
}
for (k in 1 until nGridlines) {
val pct = 1.0 - (k.toDouble() / (nGridlines - 1))
val y = paddingTop + maxBarHeight * pct
canvas.setColor(theme.lowContrastTextColor)
canvas.drawLine(0.0, y, width, y)
}
}
fun drawFooter() {
val y = paddingTop + maxBarHeight
canvas.setColor(backgroundColor)
canvas.fillRect(0.0, y, width, height - y)
canvas.setColor(theme.lowContrastTextColor)
canvas.drawLine(0.0, y, width, y)
canvas.setColor(theme.mediumContrastTextColor)
canvas.setTextAlign(TextAlign.CENTER)
var prevMonth = -1
var prevYear = -1
val isLargeInterval = (axis[0].distanceTo(axis[1]) > 300)
for (c in 0 until nColumns) {
val x = barGroupOffset(nColumns - c - 1)
val date = axis[nColumns - c - 1]
if(isLargeInterval) {
canvas.drawText(date.year.toString(),
x + barGroupWidth / 2,
y + theme.smallTextSize * 1.0)
} else {
if (date.month != prevMonth) {
canvas.drawText(dateFormatter.shortMonthName(date),
x + barGroupWidth / 2,
y + theme.smallTextSize * 1.0)
} else {
canvas.drawText(date.day.toString(),
x + barGroupWidth / 2,
y + theme.smallTextSize * 1.0)
}
if (date.year != prevYear) {
canvas.drawText(date.year.toString(),
x + barGroupWidth / 2,
y + theme.smallTextSize * 2.3)
}
}
prevMonth = date.month
prevYear = date.year
}
}
drawMajorGrid()
for (k in 0 until n) drawSeries(k)
drawFooter()
}
}

@ -33,9 +33,7 @@ class CalendarChart(var today: LocalDate,
var squareSpacing = 1.0
var series = listOf<Double>()
var scrollPosition = 0
private var squareSize = 0.0
private var fontSize = 0.0
override fun draw(canvas: Canvas) {
val width = canvas.getWidth()

@ -58,7 +58,7 @@ open class BaseViewTest {
}
} else {
actualImage.export(failedActualPath)
fail("Expected image file is missing.")
fail("Expected image file is missing. Actual image: $failedActualPath")
}
}
}

@ -0,0 +1,79 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <isoron@gmail.com>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.uhabits.components
import org.isoron.*
import org.isoron.platform.time.*
import org.isoron.uhabits.*
import kotlin.test.*
class BarChartTest : BaseViewTest() {
val base = "components/BarChart"
val today = LocalDate(2015, 1, 25)
val dailyAxis = (0..100).map { today.minus(it) }
val weeklyAxis = (0..100).map { today.minus(it * 7) }
val monthlyAxis = (0..100).map { today.minus(it * 30) }
val yearlyAxis = (0..100).map { today.minus(it * 365) }
val fmt = DependencyResolver.getDateFormatter(Locale.US)
val component = BarChart(theme, fmt)
val series1 = listOf(200.0, 80.0, 150.0, 437.0, 50.0, 80.0, 420.0,
350.0, 100.0, 375.0, 300.0, 50.0, 60.0, 350.0,
125.0)
val series2 = listOf(300.0, 500.0, 280.0, 50.0, 425.0, 300.0, 150.0,
10.0, 50.0, 200.0, 230.0, 20.0, 60.0, 34.0, 100.0)
init {
component.axis = dailyAxis
component.series.add(series1)
component.colors.add(theme.color(1))
}
@Test
fun testDraw() = asyncTest {
assertRenders(400, 200, "$base/base.png", component)
}
@Test
fun testDrawWeeklyAxis() = asyncTest {
component.axis = weeklyAxis
assertRenders(400, 200, "$base/axis-weekly.png", component)
}
@Test
fun testDrawMonthlyAxis() = asyncTest {
component.axis = monthlyAxis
assertRenders(400, 200, "$base/axis-monthly.png", component)
}
@Test
fun testDrawYearlyAxis() = asyncTest {
component.axis = yearlyAxis
assertRenders(400, 200, "$base/axis-yearly.png", component)
}
@Test
fun testDrawTwoSeries() = asyncTest {
component.series.add(series2)
component.colors.add(theme.color(3))
assertRenders(400, 200, "$base/2-series.png", component)
}
}

@ -42,9 +42,32 @@ class DetailScreenController : UITableViewController {
self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .edit,
target: self,
action: #selector(self.onEditHabitClicked))
cells.append(buildBarChartCell())
cells.append(buildHistoryChartCell())
}
func buildBarChartCell() -> UITableViewCell {
let today = LocalDate(year: 2019, month: 3, day: 15)
let axis = (0...365).map { today.minus(days: $0) }
let component = BarChart(theme: theme,
dateFormatter: IosLocalDateFormatter())
component.axis = axis
let cell = UITableViewCell()
let view = ComponentView(frame: cell.frame, component: component)
for k in 0...0 {
var series = [KotlinDouble]()
for _ in 1...365 {
series.append(KotlinDouble(value: Double.random(in: 0...5000)))
}
component.series.add(series)
let color = (self.habit.color.index + Int32(k * 3)) % 16
component.colors.add(theme.color(paletteIndex: color))
}
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
cell.contentView.addSubview(view)
return cell
}
func buildHistoryChartCell() -> UITableViewCell {
let component = CalendarChart(today: LocalDate(year: 2019, month: 3, day: 15),
color: color,

Loading…
Cancel
Save