mirror of https://github.com/iSoron/uhabits.git
parent
3e2cf48223
commit
bfea4b024a
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 16 KiB |
After Width: | Height: | Size: 17 KiB |
@ -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()
|
||||
}
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
Loading…
Reference in new issue