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