Skip to content

Commit

Permalink
Implement outside label
Browse files Browse the repository at this point in the history
Currently does not support label icon
  • Loading branch information
mahozad committed May 28, 2021
1 parent 3694866 commit d5269dd
Show file tree
Hide file tree
Showing 9 changed files with 661 additions and 65 deletions.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added piechart/inpiration/any-chart-android.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions piechart/src/main/kotlin/ir/mahozad/android/Extensions.kt
Expand Up @@ -15,3 +15,7 @@ internal fun View.pxToDp(px: Float) = px / resources.displayMetrics.density
* Converts degree to radian.
*/
internal fun Float.toRadian() = (this / 360) * 2 * PI.toFloat()

internal fun Float.toDegrees() = (this * 360) / 2 * PI.toFloat()

internal infix fun Float.until(that : Float) = this.rangeTo(that - 1E-3f)
140 changes: 111 additions & 29 deletions piechart/src/main/kotlin/ir/mahozad/android/PieChart.kt
Expand Up @@ -3,6 +3,7 @@ package ir.mahozad.android
import android.content.Context
import android.graphics.*
import android.graphics.Paint.ANTI_ALIAS_FLAG
import android.graphics.Typeface.DEFAULT
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.view.View
Expand All @@ -11,30 +12,37 @@ import androidx.annotation.Dimension
import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.graphics.minus
import ir.mahozad.android.PieChart.DrawDirection.CLOCKWISE
import ir.mahozad.android.PieChart.GapPosition.MIDDLE
import ir.mahozad.android.PieChart.GradientType.RADIAL
import ir.mahozad.android.PieChart.LabelType.INSIDE
import ir.mahozad.android.PieChart.LabelType.OUTSIDE
import ir.mahozad.android.PieChart.LegendIcons.SQUARE
import ir.mahozad.android.PieChart.SlicePointer
import java.text.NumberFormat
import kotlin.math.PI

const val DEFAULT_SIZE = 256 /* dp */
const val DEFAULT_START_ANGLE = -90
const val DEFAULT_HOLE_RATIO = 0.25f
const val DEFAULT_OVERLAY_RATIO = 0.55f
const val DEFAULT_OVERLAY_ALPHA = 0.25f
const val DEFAULT_GAP = 8f /* px */
const val DEFAULT_LABEL_SIZE = 24f /* sp */
const val DEFAULT_LABEL_ICONS_HEIGHT = DEFAULT_LABEL_SIZE /* sp */
const val DEFAULT_LABELS_SIZE = 24f /* sp */
const val DEFAULT_LABEL_ICONS_HEIGHT = DEFAULT_LABELS_SIZE /* sp */
const val DEFAULT_LABEL_ICONS_MARGIN = 8f /* dp */
const val DEFAULT_LABEL_OFFSET = 0.75f
const val DEFAULT_OUTSIDE_LABELS_MARGIN = 8f /* dp */
const val DEFAULT_CENTER_LABEL = ""
@ColorInt const val DEFAULT_LABELS_COLOR = Color.WHITE
val defaultGapPosition = MIDDLE
val defaultGradientType = RADIAL
val defaultDrawDirection = CLOCKWISE
val defaultLegendsIcon = SQUARE
val defaultLabelType = INSIDE
val defaultLabelsFont: Typeface = DEFAULT
val defaultSlicesPointer: SlicePointer? = null

/**
* This is the order that these commonly used view methods are run:
Expand Down Expand Up @@ -64,13 +72,20 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
* This color overrides the generic *labelsColor* if assigned a value other than *null*
*/
@ColorInt val labelColor: Int? = null,
@Dimension val labelSize: Float? = null,
val labelFont: Typeface? = null,

@DrawableRes val labelIcon: Int? = null,
@Dimension val labelIconHeight: Float? = null,
@Dimension val labelIconMargin: Float? = null,
@ColorInt val labelIconTint: Int? = null,
val labelIconPlacement: IconPlacement = IconPlacement.LEFT,

/**
* Distance of the start of the outside label from the pie
*/
@Dimension val outsideLabelMargin: Float? = null,
val pointer: SlicePointer? = null,
@DrawableRes val legendIcon: Int? = null,
@ColorInt val legendIconTint: Int? = null,
val legendIconPlacement: IconPlacement = IconPlacement.START,
Expand All @@ -96,7 +111,8 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
enum class IconPlacement { LEFT, RIGHT, START, END }
enum class GradientType { RADIAL, SWEEP }
enum class GapPosition { MIDDLE, PRECEDING_SLICE, SUCCEEDING_SLICE }
enum class LabelType { NONE, INSIDE, OUTSIDE, OUTSIDE_POINTER, OUTSIDE_LINE }
enum class LabelType { NONE, INSIDE, OUTSIDE, INSIDE_CIRCULAR, OUTSIDE_CIRCULAR, OUTSIDE_WITH_LINES_ON_SIDES }
data class SlicePointer(val length: Float, val width: Float, val color: Int)
interface Icon { val resId: Int }
class CustomIcon(@DrawableRes override val resId: Int) : Icon

Expand Down Expand Up @@ -156,11 +172,16 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
field = width
invalidate()
}
var labelSize = spToPx(DEFAULT_LABEL_SIZE)
var labelsSize = spToPx(DEFAULT_LABELS_SIZE)
set(size /* px */) {
field = size
invalidate()
}
var labelsFont = defaultLabelsFont
set(font) {
field = font
invalidate()
}
var labelOffset = DEFAULT_LABEL_OFFSET
set(offset) {
field = offset
Expand All @@ -176,6 +197,17 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
field = margin
invalidate()
}
var outsideLabelsMargin = dpToPx(DEFAULT_OUTSIDE_LABELS_MARGIN)
set(margin /* px */) {
field = margin
invalidate()
}
var labelType = defaultLabelType
set(type) {
field = type
invalidate()
requestLayout()
}
/**
* Is overridden by color of the slice if it is assigned a value other than *null*
*/
Expand All @@ -184,13 +216,23 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
field = color
invalidate()
}
var slicesPointer = defaultSlicesPointer
set(pointer) {
field = pointer
invalidate()
}
var legendsIcon: Icon = defaultLegendsIcon
var centerLabel = DEFAULT_CENTER_LABEL
var gapPosition = defaultGapPosition
var gradientType = defaultGradientType
var drawDirection = defaultDrawDirection
val slices = mutableListOf(
Slice(0.43f, ContextCompat.getColor(context, android.R.color.holo_green_dark)),
// Slice(fraction = 0.125f, label = "qlyO([", color = ContextCompat.getColor(context, android.R.color.holo_green_dark)),
// Slice(fraction = 0.25f, label = "qlyO([", color = ContextCompat.getColor(context, android.R.color.holo_orange_dark)),
// Slice(fraction = 0.125f, label = "qlyO([", color = ContextCompat.getColor(context, android.R.color.holo_purple)),
// Slice(fraction = 0.5f, label = "qlyO([", color = ContextCompat.getColor(context, android.R.color.holo_blue_dark)),

Slice(0.43f, ContextCompat.getColor(context, android.R.color.holo_green_dark), /*pointer = SlicePointer(50f,100f,0)*/),
Slice(0.21f, ContextCompat.getColor(context, android.R.color.holo_orange_dark)),
Slice(0.19f, ContextCompat.getColor(context, android.R.color.holo_blue_dark)),
Slice(0.14f, ContextCompat.getColor(context, android.R.color.holo_red_light)),
Expand All @@ -200,7 +242,8 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
private val clip = Path()
private val overlay = Path()
private val mainPaint: Paint = Paint(ANTI_ALIAS_FLAG).apply { style = Paint.Style.FILL }
private val enclosingRect = RectF()
private val pieEnclosingRect = RectF()
private val totalDrawableRect = RectF()
private var pieRadius = 0f
private var center = Coordinates(0f, 0f)

Expand All @@ -220,12 +263,21 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
overlayRatio = getFloat(R.styleable.PieChart_overlayRatio, DEFAULT_OVERLAY_RATIO)
overlayAlpha = getFloat(R.styleable.PieChart_overlayAlpha, DEFAULT_OVERLAY_ALPHA)
gap = getDimension(R.styleable.PieChart_gap, DEFAULT_GAP)
labelSize = getDimension(R.styleable.PieChart_labelSize, spToPx(DEFAULT_LABEL_SIZE))
labelsSize = getDimension(R.styleable.PieChart_labelsSize, spToPx(DEFAULT_LABELS_SIZE))
labelOffset = getFloat(R.styleable.PieChart_labelOffset, DEFAULT_LABEL_OFFSET)
labelsColor = getColor(R.styleable.PieChart_labelsColor, DEFAULT_LABELS_COLOR)
val fontId = getResourceId(R.styleable.PieChart_labelsFont, -1)
labelsFont = if (fontId == -1) defaultLabelsFont else ResourcesCompat.getFont(context, fontId)!!
labelIconsHeight = getDimension(R.styleable.PieChart_labelIconsHeight, spToPx(DEFAULT_LABEL_ICONS_HEIGHT))
labelIconsMargin = getDimension(R.styleable.PieChart_labelIconsMargin, dpToPx(DEFAULT_LABEL_ICONS_MARGIN))
outsideLabelsMargin = getDimension(R.styleable.PieChart_outsideLabelsMargin, dpToPx(DEFAULT_OUTSIDE_LABELS_MARGIN))
centerLabel = getString(R.styleable.PieChart_centerLabel) ?: DEFAULT_CENTER_LABEL
val slicesPointerLength = getDimension(R.styleable.PieChart_slicesPointerLength, -1f)
val slicesPointerWidth = getDimension(R.styleable.PieChart_slicesPointerWidth, -1f)
slicesPointer = if (slicesPointerLength * slicesPointerWidth < 0) defaultSlicesPointer else SlicePointer(slicesPointerLength, slicesPointerWidth, 0)
labelType = LabelType.values()[
getInt(R.styleable.PieChart_labelType, defaultLabelType.ordinal)
]
legendsIcon = LegendIcons.values()[
getInt(R.styleable.PieChart_legendsIcon, defaultLegendsIcon.ordinal)
]
Expand Down Expand Up @@ -261,12 +313,20 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {
pieRadius = calculateRadius(width, height, paddingLeft, paddingRight, paddingTop, paddingBottom)
center = calculateCenter(width, height, paddingLeft, paddingRight, paddingTop, paddingBottom)
val (top, left, right, bottom) = calculateBoundaries(center, pieRadius)
enclosingRect.set(RectF(left, top, right, bottom))
pieEnclosingRect.set(RectF(left, top, right, bottom))
totalDrawableRect.set(pieEnclosingRect)

if (labelType == OUTSIDE) {
pieEnclosingRect.set(calculatePieNewBounds(pieEnclosingRect, slices, false, outsideLabelsMargin, drawDirection, startAngle, labelsSize, labelsFont))
center = Coordinates((pieEnclosingRect.left + pieEnclosingRect.right) / 2f, (pieEnclosingRect.top + pieEnclosingRect.bottom) / 2f)
pieRadius = pieEnclosingRect.width() / 2f
}

pie.reset()
val holeRadius = holeRatio * pieRadius
val overlayRadius = overlayRatio * pieRadius
overlay.set(Path().apply { addCircle(center.x, center.y, overlayRadius, Path.Direction.CW) })
val rect = Path().apply { addRect(enclosingRect, Path.Direction.CW) }
val rect = Path().apply { addRect(totalDrawableRect, Path.Direction.CW) }
val hole = Path().apply { addCircle(center.x, center.y, holeRadius, Path.Direction.CW) }
val gaps = makeGaps()
// Could also have set the fillType to EVEN_ODD and just add the other paths to the clip
Expand Down Expand Up @@ -348,26 +408,48 @@ class PieChart(context: Context, attrs: AttributeSet) : View(context, attrs) {

mainPaint.shader = gradient
val sliceSweep = slice.fraction * 360
pie.reset()
pie.moveTo(center.x, center.y)
pie.arcTo(enclosingRect, currentAngle, sliceSweep)
canvas.drawPath(pie, mainPaint)

updatePaintForLabel(mainPaint, labelSize, slice.labelColor ?: labelsColor)
var labelIcon : Drawable? = null
slice.labelIcon?.let { iconId ->
labelIcon = resources.getDrawable(iconId, null)
slice.labelIconTint?.let { tint -> labelIcon?.setTint(tint) }
val slicePath = makeSlice(center, pieEnclosingRect, currentAngle, slice.fraction, drawDirection, slice.pointer ?: slicesPointer)
canvas.drawPath(slicePath, mainPaint)

updatePaintForLabel(mainPaint, slice.labelSize ?: labelsSize, slice.labelColor ?: labelsColor, slice.labelFont ?: labelsFont)

if (labelType == OUTSIDE) {
val middleAngle = calculateMiddleAngle(currentAngle, slice.fraction, drawDirection)
val coordinates = calculateCoordinatesForOutsideLabel(slice.label, middleAngle, center, pieRadius, outsideLabelsMargin)
canvas.drawText(slice.label, coordinates.x , coordinates.y, mainPaint)





// REMOVE this block of statements
mainPaint.style = Paint.Style.STROKE
mainPaint.color = Color.RED
val rect = RectF(coordinates.x - mainPaint.measureText(slice.label) / 2f, coordinates.y + mainPaint.ascent(), coordinates.x + mainPaint.measureText(slice.label) / 2f, coordinates.y + mainPaint.descent())
canvas.drawRect(rect, mainPaint)
mainPaint.style = Paint.Style.FILL
canvas.drawCircle(coordinates.x, coordinates.y, 4f, mainPaint)





} else {
var labelIcon : Drawable? = null
slice.labelIcon?.let { iconId ->
labelIcon = resources.getDrawable(iconId, null)
slice.labelIconTint?.let { tint -> labelIcon?.setTint(tint) }
}
val iconMargin = slice.labelIconMargin ?: labelIconsMargin
val iconHeight = slice.labelIconHeight ?: labelIconsHeight
val iconWidth = calculateLabelIconWidth(labelIcon, iconHeight)
val coordinate = calculateLabelCoordinates(currentAngle, sliceSweep, labelOffset, iconWidth, iconMargin, slice.labelIconPlacement, slice.label, mainPaint, center, pieRadius)
val labelBounds = calculateLabelBounds(slice.label, mainPaint)
val labelIconBounds = calculateLabelIconBounds(coordinate, labelBounds, iconWidth, iconHeight, iconMargin, slice.labelIconPlacement)
canvas.drawText(slice.label, coordinate.x, coordinate.y, mainPaint)
labelIcon?.setBounds(labelIconBounds.left, labelIconBounds.top, labelIconBounds.right, labelIconBounds.bottom)
labelIcon?.draw(canvas)
}
val iconMargin = slice.labelIconMargin ?: labelIconsMargin
val iconHeight = slice.labelIconHeight ?: labelIconsHeight
val iconWidth = calculateLabelIconWidth(labelIcon, iconHeight)
val coordinate = calculateLabelCoordinates(currentAngle, sliceSweep, labelOffset, iconWidth, iconMargin, slice.labelIconPlacement, slice.label, mainPaint, center, pieRadius)
val labelBounds = calculateLabelBounds(slice.label, mainPaint)
val labelIconBounds = calculateLabelIconBounds(coordinate, labelBounds, iconWidth, iconHeight, iconMargin, slice.labelIconPlacement)
canvas.drawText(slice.label, coordinate.x, coordinate.y, mainPaint)
labelIcon?.setBounds(labelIconBounds.left, labelIconBounds.top, labelIconBounds.right, labelIconBounds.bottom)
labelIcon?.draw(canvas)

currentAngle += sliceSweep
}
Expand Down

0 comments on commit d5269dd

Please sign in to comment.