Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Name refactor #43

Merged
merged 6 commits into from
May 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 22 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@ dependencies {

## Features

`DonutProgressView` is a configurable doughnut-like chart view capable of displaying multiple datasets with assignable colors. It supports animations and features a gap at the top, which makes it look like a gauge (or tasty bitten-off donut - that's why the name).
`DonutProgressView` is a configurable doughnut-like chart view capable of displaying multiple sections with assignable colors. It supports animations and features a gap at the top, which makes it look like a gauge (or tasty bitten-off donut - that's why the name).

![Header](imgs/readme-header.png)

The view automatically scales it's datasets proportionally to their values once it gets filled up. This allows you to show your users their daily progresses, reached goals, etc.
The view automatically scales it's sections proportionally to their values once it gets filled up. This allows you to show your users their daily progresses, reached goals, etc.

## Usage

Expand All @@ -45,20 +45,20 @@ Place the view in your layout
Submit data to the view

```kotlin
val dataset1 = DonutDataset(
name = "dataset_1",
val section1 = DonutSection(
name = "section_1",
color = Color.parseColor("#FB1D32"),
amount = 1f
)

val dataset2 = DonutDataset(
name = "dataset_2",
val section2 = DonutSection(
name = "section_2",
color = Color.parseColor("#FFB98E"),
amount = 1f
)

donut_view.cap = 5f
donut_view.submitData(listOf(dataset1, dataset2))
donut_view.submitData(listOf(section1, section2))
```

You'll get something like this:
Expand All @@ -67,70 +67,70 @@ You'll get something like this:

### About the data cap

Once the sum of all dataset values exceeds view's `cap` property, the view starts to scale it's datasets proportionally to their amounts along it's length. E.g. if we, in the upper example, set cap to `donut_view.cap = 1f` (`dataset1.amount + dataset2.amount > 1f`), we would get something like this:
Once the sum of all section values exceeds view's `cap` property, the view starts to scale it's sections proportionally to their amounts along it's length. E.g. if we, in the upper example, set cap to `donut_view.cap = 1f` (`section1.amount + section2.amount > 1f`), we would get something like this:

![View with cap exceeded](imgs/readme_intro_cap.png)

### Submitting data

The view accepts list of `DonutDataset` objects that define data to be displayed.
Each `DonutDataset` object holds dataset's unique name (string), it's color (color int) and dataset's value. *(Note: the view uses unique name for each dataset to resolve it's internal state and animations, and throws `IllegalStateException` if multiple datasets with same name are submitted.)*
The view accepts list of `DonutSection` objects that define data to be displayed.
Each `DonutSection` object holds section's unique name (string), it's color (color int) and section's value. *(Note: the view uses unique name for each section to resolve it's internal state and animations, and throws `IllegalStateException` if multiple sections with same name are submitted.)*

```kotlin
val waterAmount = DonutDataset(
val waterAmount = DonutSection(
name = "drink_amount_water",
color = Color.parseColor("#03BFFA"),
amount = 1.2f
)
```

You have to submit new list of datasets everytime you want to modify displayed data, as `DonutDataset` object is immutable.
You have to submit new list of sections everytime you want to modify displayed data, as `DonutSection` object is immutable.

```kotlin
donut_view.submitData(listOf(waterAmount))
```

#### Granular controls

The view also provides methods for more granular control over displayed data. You can use `addAmount`, `setAmount` and `removeAmount` methods to add, set or remove specified amounts from displayed datasets.
The view also provides methods for more granular control over displayed data. You can use `addAmount`, `setAmount` and `removeAmount` methods to add, set or remove specified amounts from displayed sections.

##### Adding amount

```kotlin
donut_view.addAmount(
datasetName = "drink_amount_water",
sectionName = "drink_amount_water",
amount = 0.5f,
color = Color.parseColor("#03BFFA") // Optional, pass color if you want to create new dataset
color = Color.parseColor("#03BFFA") // Optional, pass color if you want to create new section
)
```

The `addAmount` adds specified amount to dataset with provided name. What if dataset does not yet exist? This method has one optional `color` parameter (default value is `null`) - when called, and there isn't already displayed any dataset with provided name and `color` parameter was specified, the new `DonutDataset` with provided name, amount and color will be automatically created internally for you. If you leave the `color` param `null` while trying to add value to non-existent dataset, nothing happens.
The `addAmount` adds specified amount to section with provided name. What if section does not yet exist? This method has one optional `color` parameter (default value is `null`) - when called, and there isn't already displayed any section with provided name and `color` parameter was specified, the new `DonutSection` with provided name, amount and color will be automatically created internally for you. If you leave the `color` param `null` while trying to add value to non-existent section, nothing happens.

##### Setting amount

```kotlin
donut_view.setAmount(
datasetName = "drink_amount_water",
sectionName = "drink_amount_water",
amount = 2.5f
)
```

The `setAmount` methods sets specified amount to dataset with provided name. If provided amount is equal or less than 0, dataset and corresponding progress line are automatically removed after animation. If view does not contain specified dataset, nothing happens.
The `setAmount` methods sets specified amount to section with provided name. If provided amount is equal or less than 0, section and corresponding progress line are automatically removed after animation. If view does not contain specified section, nothing happens.

##### Removing amount

```kotlin
donut_view.removeAmount(
datasetName = "drink_amount_water",
sectionName = "drink_amount_water",
amount = 0.1f
)
```

The `removeAmount` simply subtracts specified amount from any displayed dataset. If resulting amount is equal or less than 0, dataset and corresponding progress line are automatically removed after animation. If view does not contain specified dataset, nothing happens.
The `removeAmount` simply subtracts specified amount from any displayed section. If resulting amount is equal or less than 0, section and corresponding progress line are automatically removed after animation. If view does not contain specified section, nothing happens.

#### Get and clear data

If you want to get currently displayed data, call `getData()` method which returns immutable list of all displayed `DonutDataset` objects. To clear displayed data, call `clear()` method.
If you want to get currently displayed data, call `getData()` method which returns immutable list of all displayed `DonutSection` objects. To clear displayed data, call `clear()` method.

Each call to a data method (submit, add, set, remove, clear) results in view **automatically resolving and animating to the new state**.

Expand All @@ -143,7 +143,7 @@ The view allows you to configure various properties to let you create a unique s
|Name|Default value|Description|
|---|---|---|
| `donut_cap`| `1.0f` | View's cap property |
| `donut_strokeWidth` | `12dp` | Width of background and dataset lines in dp |
| `donut_strokeWidth` | `12dp` | Width of background and section lines in dp |
| `donut_bgLineColor`| `#e7e8e9` | Color of background line |
| `donut_gapWidth` | `45°` | Width of the line gap in degrees |
| `donut_gapAngle` | `90°` | Position of the line gap around the view in degrees |
Expand Down
10 changes: 0 additions & 10 deletions library/src/main/kotlin/app/futured/donut/DonutDataset.kt

This file was deleted.

86 changes: 43 additions & 43 deletions library/src/main/kotlin/app/futured/donut/DonutProgressView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class DonutProgressView @JvmOverloads constructor(
*/
var animationDurationMs: Long = DEFAULT_ANIM_DURATION_MS.toLong()

private val data = mutableListOf<DonutDataset>()
private val data = mutableListOf<DonutSection>()
private val lines = mutableListOf<DonutProgressLine>()
private var animatorSet: AnimatorSet? = null

Expand Down Expand Up @@ -219,23 +219,23 @@ class DonutProgressView @JvmOverloads constructor(
fun getData() = data.toList()

/**
* Submits new [datasets] to the view.
* Submits new [sections] to the view.
*
* New progress line will be created for each non-existent dataset and view will be animated to new state.
* New progress line will be created for each non-existent section and view will be animated to new state.
* Additionally, existing lines with no data set will be removed when animation completes.
*/
fun submitData(datasets: List<DonutDataset>) {
assertDataConsistency(datasets)
fun submitData(sections: List<DonutSection>) {
assertDataConsistency(sections)

datasets
sections
.filter { it.amount > 0f }
.forEach { dataset ->
val newLineColor = dataset.color
if (hasEntriesForDataset(dataset.name).not()) {
.forEach { section ->
val newLineColor = section.color
if (hasEntriesForSection(section.name).not()) {
lines.add(
index = 0,
element = DonutProgressLine(
name = dataset.name,
name = section.name,
radius = radius,
lineColor = newLineColor,
lineStrokeWidth = strokeWidth,
Expand All @@ -247,13 +247,13 @@ class DonutProgressView @JvmOverloads constructor(
)
} else {
lines
.filter { it.name == dataset.name }
.filter { it.name == section.name }
.forEach { it.mLineColor = newLineColor }
}
}

this.data.apply {
val copy = ArrayList(datasets)
val copy = ArrayList(sections)
clear()
addAll(copy)
}
Expand All @@ -262,12 +262,12 @@ class DonutProgressView @JvmOverloads constructor(
}

/**
* Adds [amount] to existing dataset specified by [datasetName]. If dataset does not exist and [color] is specified,
* creates new dataset internally.
* Adds [amount] to existing section specified by [sectionName]. If section does not exist and [color] is specified,
* creates new section internally.
*/
fun addAmount(datasetName: String, amount: Float, color: Int? = null) {
fun addAmount(sectionName: String, amount: Float, color: Int? = null) {
for (i in 0 until data.size) {
if (data[i].name == datasetName) {
if (data[i].name == sectionName) {
data[i] = data[i].copy(amount = data[i].amount + amount)
submitData(data)
return
Expand All @@ -276,27 +276,27 @@ class DonutProgressView @JvmOverloads constructor(

color?.let {
submitData(
data + DonutDataset(
name = datasetName,
data + DonutSection(
name = sectionName,
color = it,
amount = amount
)
)
}
?: warn {
"Adding amount to non-existent dataset: $datasetName. " +
"Please specify color, if you want to have dataset created automatically."
"Adding amount to non-existent section: $sectionName. " +
"Please specify color, if you want to have section created automatically."
}
}

/**
* Sets [amount] for existing dataset specified by [datasetName].
* Removes dataset if amount is <= 0.
* Does nothing if dataset does not exist.
* Sets [amount] for existing section specified by [sectionName].
* Removes section if amount is <= 0.
* Does nothing if section does not exist.
*/
fun setAmount(datasetName: String, amount: Float) {
fun setAmount(sectionName: String, amount: Float) {
for (i in 0 until data.size) {
if (data[i].name == datasetName) {
if (data[i].name == sectionName) {
if (amount > 0) {
data[i] = data[i].copy(amount = amount)
} else {
Expand All @@ -307,16 +307,16 @@ class DonutProgressView @JvmOverloads constructor(
}
}

warn { "Setting amount for non-existent dataset: $datasetName" }
warn { "Setting amount for non-existent section: $sectionName" }
}

/**
* Removes [amount] from existing dataset specified by [datasetName].
* If amount gets below zero, removes the dataset from view.
* Removes [amount] from existing section specified by [sectionName].
* If amount gets below zero, removes the section from view.
*/
fun removeAmount(datasetName: String, amount: Float) {
fun removeAmount(sectionName: String, amount: Float) {
for (i in 0 until data.size) {
if (data[i].name == datasetName) {
if (data[i].name == sectionName) {
val resultAmount = data[i].amount - amount
if (resultAmount > 0) {
data[i] = data[i].copy(amount = resultAmount)
Expand All @@ -328,39 +328,39 @@ class DonutProgressView @JvmOverloads constructor(
}
}

warn { "Removing amount from non-existent dataset: $datasetName" }
warn { "Removing amount from non-existent section: $sectionName" }
}

/**
* Clear data, removing all lines.
*/
fun clear() = submitData(listOf())

private fun assertDataConsistency(data: List<DonutDataset>) {
private fun assertDataConsistency(data: List<DonutSection>) {
if (data.hasDuplicatesBy { it.name }) {
throw IllegalStateException("Multiple datasets with same name found")
throw IllegalStateException("Multiple sections with same name found")
}
}

private fun resolveState() {
animatorSet?.cancel()
animatorSet = AnimatorSet()

val datasetAmounts = lines.map { getAmountForDataset(it.name) }
val totalAmount = datasetAmounts.sumByFloat { it }
val sectionAmounts = lines.map { getAmountForSection(it.name) }
val totalAmount = sectionAmounts.sumByFloat { it }

val drawPercentages = datasetAmounts.mapIndexed { index, _ ->
val drawPercentages = sectionAmounts.mapIndexed { index, _ ->
if (totalAmount > cap) {
getDrawAmountForLine(datasetAmounts, index) / totalAmount
getDrawAmountForLine(sectionAmounts, index) / totalAmount
} else {
getDrawAmountForLine(datasetAmounts, index) / cap
getDrawAmountForLine(sectionAmounts, index) / cap
}
}

drawPercentages.forEachIndexed { index, newPercentage ->
val line = lines[index]
val animator = animateLine(line, newPercentage) {
if (!hasEntriesForDataset(line.name)) {
if (!hasEntriesForSection(line.name)) {
removeLine(line)
}
}
Expand All @@ -371,9 +371,9 @@ class DonutProgressView @JvmOverloads constructor(
animatorSet?.start()
}

private fun getAmountForDataset(dataset: String): Float {
private fun getAmountForSection(sectionName: String): Float {
return data
.filter { it.name == dataset }
.filter { it.name == sectionName }
.sumByFloat { it.amount }
}

Expand All @@ -388,8 +388,8 @@ class DonutProgressView @JvmOverloads constructor(
return thisLine + previousLine
}

private fun hasEntriesForDataset(dataset: String) =
data.indexOfFirst { it.name == dataset } > -1
private fun hasEntriesForSection(section: String) =
data.indexOfFirst { it.name == section } > -1

private fun animateLine(
line: DonutProgressLine,
Expand Down
10 changes: 10 additions & 0 deletions library/src/main/kotlin/app/futured/donut/DonutSection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package app.futured.donut

/**
* Data class representing section of the graph containing [Float] amount, name and color of progress line.
*/
data class DonutSection(
val name: String,
val color: Int,
val amount: Float
)
Loading