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

Add support for rounded corners for BarChart #3754

Closed
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,17 @@ open class BarChartDataSet: BarLineScatterCandleBubbleChartDataSet, IBarChartDat
open var barShadowColor = NSUIColor(red: 215.0/255.0, green: 215.0/255.0, blue: 215.0/255.0, alpha: 1.0)

/// the width used for drawing borders around the bars. If borderWidth == 0, no border will be drawn.
open var barBorderWidth : CGFloat = 0.0
open var barBorderWidth: CGFloat = 0.0

/// the color drawing borders around the bars.
open var barBorderColor = NSUIColor.black

/// the alpha value (transparency) that is used for drawing the highlight indicator bar. min = 0.0 (fully transparent), max = 1.0 (fully opaque)
open var highlightAlpha = CGFloat(120.0 / 255.0)

/// corners to be rounded
open var roundedCorners: UIRectCorner = []

// MARK: - NSCopying

open override func copy(with zone: NSZone? = nil) -> Any
Expand Down
3 changes: 3 additions & 0 deletions Source/Charts/Data/Interfaces/IBarChartDataSet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@ public protocol IBarChartDataSet: IBarLineScatterCandleBubbleChartDataSet

/// array of labels used to describe the different values of the stacked bars
var stackLabels: [String] { get set }

/// corners to be rounded
var roundedCorners: UIRectCorner { get set }
}
144 changes: 105 additions & 39 deletions Source/Charts/Renderers/BarChartRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -377,9 +377,9 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
// draw the bar shadow before the values
if dataProvider.isDrawBarShadowEnabled
{
for j in stride(from: 0, to: buffer.rects.count, by: 1)
for firstIndexInBar in stride(from: 0, to: buffer.rects.count, by: 1)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why change name here? seems nothing about first index

Copy link
Collaborator

@jjatie jjatie Dec 25, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have the same question. If anything the change should be:

 for barRect in buffer.rects
    where viewPortHandler.isInBoundsLeft(barRect.maxX)
{
    guard viewPortHandler.isInBoundsRight(barRect.origin.x) else
    {
          break
    }
    context.setFillColor(dataSet.barShadowColor.cgColor)
    context.fill(barRect)
}

{
let barRect = buffer.rects[j]
let barRect = buffer.rects[firstIndexInBar]

if (!viewPortHandler.isInBoundsLeft(barRect.origin.x + barRect.size.width))
{
Expand All @@ -402,53 +402,81 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
{
context.setFillColor(dataSet.color(atIndex: 0).cgColor)
}

context.setStrokeColor(borderColor.cgColor)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it seems you are using drawBorder properties for rounded corners. Why force setting setLineCap(.square)?

But what if people disable drawBorder?

context.setLineWidth(borderWidth)
context.setLineCap(.square)

// In case the chart is stacked, we need to accomodate individual bars within accessibilityOrdereredElements
let isStacked = dataSet.isStacked
let stackSize = isStacked ? dataSet.stackSize : 1

for j in stride(from: 0, to: buffer.rects.count, by: 1)
for firstIndexInBar in stride(from: 0, to: buffer.rects.count, by: stackSize)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why stackSize here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it was done for the stacked bar char case, if round each bar when we will have this situation (from my rounding implementation).
StackedBarChar_rounded
Author's example:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like it was done for the stacked bar char case, if round each bar when we will have this situation (from my rounding implementation).
StackedBarChar_rounded
Author's example:

That's exactly what I wanted to do. Round corner each bar with space in between. Do you have a demo project?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @panncherry
You can check my fork/branch. Do not remember if bothered with an example adjustment as not going to upload a pull request, but the changes should be pretty straightforward to update existing StackedBarChar from the ChartsDemo-iOS

{
let barRect = buffer.rects[j]
context.saveGState()

let lastIndexInBar = firstIndexInBar + stackSize - 1


if (!viewPortHandler.isInBoundsLeft(barRect.origin.x + barRect.size.width))
{
continue
}
let topRectInBar = findTopRectInBar(barRects: buffer.rects,
firstIndexInBar: firstIndexInBar,
lastIndexInBar: lastIndexInBar)

let path = createBarPath(for: topRectInBar, roundedCorners: dataSet.roundedCorners)

if (!viewPortHandler.isInBoundsRight(barRect.origin.x))
{
break
}
context.addPath(path.cgPath)
context.clip()

if !isSingleColor
{
// Set the color for the currently drawn value. If the index is out of bounds, reuse colors.
context.setFillColor(dataSet.color(atIndex: j).cgColor)
for index in firstIndexInBar...lastIndexInBar {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let barRects = buffer.rects[firstIndexInBar...lastIndexInBar]
for (index, rect) in zip(barRects, barRects.indices)
    where viewPortHandler.isInBoundsLeft(barRect.maxX)
{
    ...
}


let barRect = buffer.rects[index]

if (!viewPortHandler.isInBoundsLeft(barRect.origin.x + barRect.size.width))
{
continue
}

if (!viewPortHandler.isInBoundsRight(barRect.origin.x))
{
break
}

if !isSingleColor
{
// Set the color for the currently drawn value. If the index is out of bounds, reuse colors.
context.setFillColor(dataSet.color(atIndex: index).cgColor)
}

context.addRect(barRect)
context.fillPath()

if drawBorder {
context.stroke(barRect)
}

// Create and append the corresponding accessibility element to accessibilityOrderedElements
if let chart = dataProvider as? BarChartView
{
let element = createAccessibleElement(withIndex: index,
container: chart,
dataSet: dataSet,
dataSetIndex: index,
stackSize: stackSize)
{ (element) in
element.accessibilityFrame = barRect
}

accessibilityOrderedElements[index/stackSize].append(element)
}

}

context.fill(barRect)
context.restoreGState()

if drawBorder
{
context.setStrokeColor(borderColor.cgColor)
context.setLineWidth(borderWidth)
context.stroke(barRect)
}

// Create and append the corresponding accessibility element to accessibilityOrderedElements
if let chart = dataProvider as? BarChartView
{
let element = createAccessibleElement(withIndex: j,
container: chart,
dataSet: dataSet,
dataSetIndex: index,
stackSize: stackSize)
{ (element) in
element.accessibilityFrame = barRect
}

accessibilityOrderedElements[j/stackSize].append(element)
context.addPath(path.cgPath)
context.strokePath()
}
}

Expand Down Expand Up @@ -804,12 +832,22 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer
y1 = e.y
y2 = 0.0
}

prepareBarHighlight(x: e.x, y1: e.positiveSum, y2: -e.negativeSum, barWidthHalf: barData.barWidth / 2.0, trans: trans, rect: &barRect)

prepareBarHighlight(x: e.x, y1: y1, y2: y2, barWidthHalf: barData.barWidth / 2.0, trans: trans, rect: &barRect)

setHighlightDrawPos(highlight: high, barRect: barRect)
var highlightRect = CGRect()
prepareBarHighlight(x: e.x, y1: y1, y2: y2, barWidthHalf: barData.barWidth / 2.0, trans: trans, rect: &highlightRect)
setHighlightDrawPos(highlight: high, barRect: highlightRect)

context.fill(barRect)
let path = createBarPath(for: barRect, roundedCorners: set.roundedCorners)

context.saveGState()

context.addPath(path.cgPath)
context.clip()
context.fill(highlightRect)

context.restoreGState()
}
}

Expand Down Expand Up @@ -896,4 +934,32 @@ open class BarChartRenderer: BarLineScatterCandleBubbleRenderer

return element
}

private func findTopRectInBar(barRects: [CGRect], firstIndexInBar: Int, lastIndexInBar: Int) -> CGRect {
Copy link
Member

@liuxuan30 liuxuan30 Dec 24, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's put all { to another line. This is the code style for this library

var topRectInBar = barRects[firstIndexInBar]
if barRects[lastIndexInBar].origin.y < topRectInBar.origin.y {
topRectInBar = barRects[lastIndexInBar]
}

var height: CGFloat = 0
for index in firstIndexInBar...lastIndexInBar {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

topRectInBar.size.height = barRects[firstIndexInBar...lastIndexInBar]
    .reduce(0) { $0 + $1.height }

height += barRects[index].height
}

topRectInBar.size.height = height

return topRectInBar
}

/// Creates path for bar in rect with rounded corners
internal func createBarPath(for rect: CGRect, roundedCorners: UIRectCorner) -> UIBezierPath {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

internal is default and is not needed.


let cornerRadius = rect.width / 2.0

let path = UIBezierPath(roundedRect: rect,

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@skarol I see a problem here when the rect is smaller than the cornerRadius, in this case you'll have a smashed rounded rectangle that looks bad(I haven't tested this code, but I worked in this feature in my fork, and at the start I had used the UIBezierPath like you with the result that I have commented, some screenshots at the end, apologies for the quality).
My proposal to solve this is to split it into two cases(Take into consideration that I haven't used the open var roundedCorners: UIRectCorner = []. I will always round all corners)

  1. rect.height >= rect.width in this case we can perform the UIBezierPath as you do
  2. rect.height < rect.width in this case we can draw a portion of a circumference, I leave the algorithm
let height = rect.size.height
let hypotenuse = width / 2.0
let radius = hypotenuse
let startAngle: CGFloat
let endAngle: CGFloat
if height < radius {
    let oLeg = radius - height
    startAngle = asin(oLeg/hypotenuse)
    endAngle = CGFloat(Double.pi) - startAngle
} else {
    let oLeg = height - radius
    startAngle = -asin(oLeg/hypotenuse)
    endAngle = CGFloat(Double.pi) - startAngle
}
let y = rect.origin.y - (width - rect.size.height)
let center = CGPoint(x: rect.origin.x + radius, y: y + radius)
let bezierPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)

Before
before2
before3

After
share1
share

byRoundingCorners: roundedCorners,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))

return path
}
}
126 changes: 90 additions & 36 deletions Source/Charts/Renderers/HorizontalBarChartRenderer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -240,53 +240,80 @@ open class HorizontalBarChartRenderer: BarChartRenderer
{
context.setFillColor(dataSet.color(atIndex: 0).cgColor)
}

context.setStrokeColor(borderColor.cgColor)
context.setLineWidth(borderWidth)
context.setLineCap(.square)

// In case the chart is stacked, we need to accomodate individual bars within accessibilityOrdereredElements
// In case the chart is stacked, we need to accomodate individual bars within accessibilityOrderedElements
let isStacked = dataSet.isStacked
let stackSize = isStacked ? dataSet.stackSize : 1

for j in stride(from: 0, to: buffer.rects.count, by: 1)
for firstIndexInBar in stride(from: 0, to: buffer.rects.count, by: stackSize)
{
let barRect = buffer.rects[j]
context.saveGState()

if (!viewPortHandler.isInBoundsTop(barRect.origin.y + barRect.size.height))
{
break
}
let lastIndexInBar = firstIndexInBar + stackSize - 1

let leftRectInBar = findMostLeftRectInBar(barRects: buffer.rects,
firstIndexInBar: firstIndexInBar,
lastIndexInBar: lastIndexInBar)

let path = createBarPath(for: leftRectInBar, roundedCorners: dataSet.roundedCorners)

if (!viewPortHandler.isInBoundsBottom(barRect.origin.y))
{
continue
}
context.addPath(path.cgPath)
context.clip()

if !isSingleColor
{
// Set the color for the currently drawn value. If the index is out of bounds, reuse colors.
context.setFillColor(dataSet.color(atIndex: j).cgColor)
for index in firstIndexInBar...lastIndexInBar {

let barRect = buffer.rects[index]

if (!viewPortHandler.isInBoundsLeft(barRect.origin.x + barRect.size.width))
{
continue
}

if (!viewPortHandler.isInBoundsRight(barRect.origin.x))
{
break
}

if !isSingleColor
{
// Set the color for the currently drawn value. If the index is out of bounds, reuse colors.
context.setFillColor(dataSet.color(atIndex: index).cgColor)
}

context.addRect(barRect)
context.fillPath()

if drawBorder {
context.stroke(barRect)
}

// Create and append the corresponding accessibility element to accessibilityOrderedElements
if let chart = dataProvider as? BarChartView
{
let element = createAccessibleElement(withIndex: index,
container: chart,
dataSet: dataSet,
dataSetIndex: index,
stackSize: stackSize)
{ (element) in
element.accessibilityFrame = barRect
}

accessibilityOrderedElements[index/stackSize].append(element)
}

}

context.fill(barRect)

context.restoreGState()
if drawBorder
{
context.setStrokeColor(borderColor.cgColor)
context.setLineWidth(borderWidth)
context.stroke(barRect)
}

// Create and append the corresponding accessibility element to accessibilityOrderedElements (see BarChartRenderer)
if let chart = dataProvider as? BarChartView
{
let element = createAccessibleElement(withIndex: j,
container: chart,
dataSet: dataSet,
dataSetIndex: index,
stackSize: stackSize)
{ (element) in
element.accessibilityFrame = barRect
}

accessibilityOrderedElements[j/stackSize].append(element)
context.addPath(path.cgPath)
context.strokePath()
}
}

Expand Down Expand Up @@ -628,4 +655,31 @@ open class HorizontalBarChartRenderer: BarChartRenderer
{
high.setDraw(x: barRect.midY, y: barRect.origin.x + barRect.size.width)
}

override internal func createBarPath(for rect: CGRect, roundedCorners: UIRectCorner) -> UIBezierPath {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could bar renderer and horizontal bar renderer share the same createBarPath and findMostLeftRectInBar somehow?


let cornerRadius = rect.height / 2.0

let path = UIBezierPath(roundedRect: rect,
byRoundingCorners: roundedCorners,
cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))

return path
}

private func findMostLeftRectInBar(barRects: [CGRect], firstIndexInBar: Int, lastIndexInBar: Int) -> CGRect {
var leftRectInBar = barRects[firstIndexInBar]
if barRects[lastIndexInBar].origin.x < leftRectInBar.origin.x {
leftRectInBar = barRects[lastIndexInBar]
}

var width: CGFloat = 0
for index in firstIndexInBar...lastIndexInBar {
width += barRects[index].width
}

leftRectInBar.size.width = width

return leftRectInBar
}
}
8 changes: 8 additions & 0 deletions Tests/Charts/BarChartTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -339,4 +339,12 @@ class BarChartTests: FBSnapshotTestCase
chart.notifyDataSetChanged()
FBSnapshotVerifyView(chart, identifier: Snapshot.identifier(UIScreen.main.bounds.size), tolerance: Snapshot.tolerance)
}

func testRoundedCorners() {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not seeing updated images for testing rounded corners.

let dataEntries = setupDefaultValuesDataEntries()
let dataSet = setupDefaultDataSet(chartDataEntries: dataEntries)
let chart = setupDefaultChart(dataSets: [dataSet])
dataSet.roundedCorners = [.allCorners]
FBSnapshotVerifyView(chart, identifier: Snapshot.identifier(UIScreen.main.bounds.size), tolerance: Snapshot.tolerance)
}
}