Skip to content

Commit

Permalink
core: Implement bar chart
Browse files Browse the repository at this point in the history
  • Loading branch information
iSoron committed Aug 18, 2019
1 parent 3e2cf48 commit bfea4b0
Show file tree
Hide file tree
Showing 11 changed files with 271 additions and 7 deletions.
Binary file added core/assets/test/components/BarChart/2-series.png
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
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.
Binary file added core/assets/test/components/BarChart/base.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions core/src/main/common/org/isoron/platform/time/Dates.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
164 changes: 164 additions & 0 deletions core/src/main/common/org/isoron/uhabits/components/BarChart.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <[email protected]>
*
* 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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 1 addition & 1 deletion core/src/test/common/org/isoron/uhabits/BaseViewTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
}
79 changes: 79 additions & 0 deletions core/src/test/common/org/isoron/uhabits/components/BarChartTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright (C) 2016-2019 Álinson Santos Xavier <[email protected]>
*
* 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)
}
}
23 changes: 23 additions & 0 deletions ios/Application/Frontend/DetailScreenController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down

0 comments on commit bfea4b0

Please sign in to comment.