Skip to content

Commit

Permalink
v0.7.1: segmented line animation fix + selected bar gradient in bar c…
Browse files Browse the repository at this point in the history
…hart settings

- A segmented line with several color-segments now appears with a continuous draw-style animation from left to right.
- Added selectedBarGradient property to DYBarSettings. Setting this value will apply a separate linear gradient to the selected bar.
- Code optimisation
  • Loading branch information
DominikButz committed Jun 13, 2022
1 parent 15ceeb8 commit c0c42e6
Show file tree
Hide file tree
Showing 6 changed files with 76 additions and 60 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,11 @@ struct RingChartAndDetailPieChartExample: View {

## Change log

#### [Version 0.7.1](https://github.com/DominikButz/SwiftUIGraphs/releases/tag/0.7.1)
- A segmented line with several color-segments now appears with a continuous draw-style animation from left to right.
- Added selectedBarGradient property to DYBarSettings. Setting this value will apply a separate linear gradient to the selected bar.
- Code optimisation

#### [Version 0.7](https://github.com/DominikButz/SwiftUIGraphs/releases/tag/0.7)
It is now possible to set individual colors per data point and per line section using closures in the initialiser of DYLineChartView. Additionally, it is possible to set a different color per each bar in the DYBarChartView initialiser. Special thanks to SAleksiev for his suggestion and help.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,12 +101,14 @@ public struct DYBarChartSettings: DYGridChartSettings {

public var chartViewBackgroundColor: Color
public var gradient: LinearGradient

public var lateralPadding: (leading: CGFloat, trailing: CGFloat)
public var yAxisSettings: YAxisSettings
public var xAxisSettings: XAxisSettings

var showSelectionIndicator: Bool
var selectionIndicatorColor: Color
public var selectedBarGradient: LinearGradient?

/// Initializer of DYBarChartSettings
/// - Parameters:
Expand All @@ -115,12 +117,14 @@ public struct DYBarChartSettings: DYGridChartSettings {
/// - lateralPadding: adds padding, leading and trailing, before the first and after the last bar.
/// - showSelectionIndicator: determines if the selection indicator should be shown at the top of the grid. selection changes on bar tap.
/// - selectionIndicatorColor: color of the selection indicator.
/// - selectedBarGradient: Linear gradient for the selected bar. Default is nil (no different gradient for the selected bar).
/// - yAxisSettings: y-axis settings
/// - xAxisSettings: x-axis settings
public init(chartViewBackgroundColor: Color = Color(.systemBackground), gradient: LinearGradient = LinearGradient(gradient: Gradient(colors: [Color.orange, Color.orange.opacity(0.8)]), startPoint: .top, endPoint: .bottom), lateralPadding: (leading: CGFloat, trailing: CGFloat) = (0, 0), showSelectionIndicator: Bool = true, selectionIndicatorColor: Color = .orange, yAxisSettings: YAxisSettings = YAxisSettings(), xAxisSettings: DYBarChartXAxisSettings = DYBarChartXAxisSettings()) {
public init(chartViewBackgroundColor: Color = Color(.systemBackground), gradient: LinearGradient = LinearGradient(gradient: Gradient(colors: [Color.orange, Color.orange.opacity(0.8)]), startPoint: .top, endPoint: .bottom), lateralPadding: (leading: CGFloat, trailing: CGFloat) = (0, 0), showSelectionIndicator: Bool = true, selectionIndicatorColor: Color = .orange, selectedBarGradient: LinearGradient? = nil, yAxisSettings: YAxisSettings = YAxisSettings(), xAxisSettings: DYBarChartXAxisSettings = DYBarChartXAxisSettings()) {

self.chartViewBackgroundColor = chartViewBackgroundColor
self.gradient = gradient
self.selectedBarGradient = selectedBarGradient
self.lateralPadding = lateralPadding
self.showSelectionIndicator = showSelectionIndicator
self.selectionIndicatorColor = selectionIndicatorColor
Expand Down
9 changes: 5 additions & 4 deletions Sources/SwiftUIGraphs/Views/Bar Chart/DYBarChartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public struct DYBarChartView: View, DYGridChart {
/// - gradientPerBar: overrides the gradient in the DYBarChartSettings for each individual bar. Default value is nil (all bars are filled with the gradient of the DYBarChartSettings.
/// - chartFrameHeight: the height of the chart (including x-axis, if applicable). If an the x-axis view is present, it is recommended to set this value, otherwise the height might be unpredictable.
/// - settings: DYBarChart settings.
public init(dataPoints: [DYDataPoint], selectedIndex: Binding<Int>, xValueConverter: @escaping (Double)->String, yValueConverter: @escaping (Double)->String, gradientPerBar: ((DYDataPoint)->LinearGradient)? = nil, chartFrameHeight:CGFloat? = nil, settings: DYBarChartSettings = DYBarChartSettings()) {
public init(dataPoints: [DYDataPoint], selectedIndex: Binding<Int>, xValueConverter: @escaping (Double)->String, yValueConverter: @escaping (Double)->String, gradientPerBar: ((DYDataPoint)->LinearGradient)? = nil, chartFrameHeight:CGFloat? = nil, settings: DYBarChartSettings = DYBarChartSettings()) {
self._selectedIndex = selectedIndex
self.xValueConverter = xValueConverter
self.yValueConverter = yValueConverter
Expand Down Expand Up @@ -117,7 +117,7 @@ public struct DYBarChartView: View, DYGridChart {
.matchedGeometryEffect(id: "selectionIndicator", in: namespace)
}
Spacer(minLength: 0)
BarView(gradient: gradientPerBar?(dataPoint) ?? settings.gradient, width: barWidth, height: self.convertToYCoordinate(value: dataPoint.yValue, height: height), index: i, orientationObserver: self.orientationObserver, selectedIndex: self.$selectedIndex, selectionFeedbackGenerator: self.generator)
BarView(gradient: gradientPerBar?(dataPoint) ?? settings.gradient, selectedBarGradient: (settings as! DYBarChartSettings).selectedBarGradient, width: barWidth, height: self.convertToYCoordinate(value: dataPoint.yValue, height: height), index: i, orientationObserver: self.orientationObserver, selectedIndex: self.$selectedIndex, selectionFeedbackGenerator: self.generator)
}
Spacer(minLength: 0)

Expand Down Expand Up @@ -185,6 +185,7 @@ public struct DYBarChartView: View, DYGridChart {
internal struct BarView: View {

var gradient: LinearGradient
var selectedBarGradient: LinearGradient? = nil
var width: CGFloat
var height: CGFloat
var index: Int
Expand All @@ -198,7 +199,7 @@ internal struct BarView: View {
var body: some View {

RoundedCornerRectangle(tl: 5, tr: 5, bl: 0, br: 0)
.fill(gradient)
.fill(selectedIndex == index ? (selectedBarGradient ?? gradient) : gradient)
.frame(width: width, height: self.currentHeight, alignment: .bottom)
.scaleEffect(self.scale, anchor: .bottom) // for selection scale effect
.onTapGesture {
Expand Down Expand Up @@ -229,8 +230,8 @@ internal struct BarView: View {

DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
withAnimation(Animation.spring()) {
self.scale = 1
self.selectedIndex = index
self.scale = 1
}
}

Expand Down
112 changes: 59 additions & 53 deletions Sources/SwiftUIGraphs/Views/Line Chart/DYLineChartView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@ public struct DYLineChartView: View, DYGridChart {

@Binding var selectedIndex: Int

@State var lineOffset: CGFloat = 0 // Vertical line offset
@State var selectedYPos: CGFloat = 0 // User Y touch location
@State var isSelected: Bool = false // Is the user touching the graph
@State private var lineOffset: CGFloat = 0 // Vertical line offset
@State private var selectedYPos: CGFloat = 0 // User Y touch location
@State private var isSelected: Bool = false // Is the user touching the graph
@State private var lineEnd: CGFloat = 0 // for line animation
@State var showWithAnimation: Bool = false
@State private var showLineSegments: Bool = false
@State private var showWithAnimation: Bool = false

var chartFrameHeight: CGFloat?
var settings: DYGridChartSettings
Expand Down Expand Up @@ -99,7 +100,7 @@ public struct DYLineChartView: View, DYGridChart {
}

if let _ = self.colorPerLineSegment {
self.lines()
self.lineSegments()
} else {
self.line()
}
Expand Down Expand Up @@ -130,10 +131,11 @@ public struct DYLineChartView: View, DYGridChart {
self.xAxisView()
}
}
.transition(AnyTransition.opacity)
//.transition(AnyTransition.opacity)
.onAppear {
withAnimation(.easeInOut(duration: 1.4)) {
self.lineEnd = 1
self.showLineSegments = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 1.4) {
self.showWithAnimation = true
Expand Down Expand Up @@ -222,31 +224,39 @@ public struct DYLineChartView: View, DYGridChart {

}

// new!
private func lines()-> some View {
// for separate
private func lineSegments()-> some View {
GeometryReader { geo in
Group {
if self.dataPoints.count >= 2 {
ForEach(0..<dataPoints.count ) { index in

Path { path in
path = self.drawLineWith(path: &path, index: index, height: geo.size.height, width: geo.size.width)

Group {
if self.dataPoints.count >= 2 {
ForEach(0..<dataPoints.count ) { index in

Path { path in
path = self.drawPathWith(path: &path, index: index, height: geo.size.height, width: geo.size.width)

}
.stroke(style: (self.settings as! DYLineChartSettings).lineStrokeStyle)
.foregroundColor(self.colorPerLineSegment!(dataPoints[index]))

}
.trim(from: 0, to: self.lineEnd)
.stroke(style: (self.settings as! DYLineChartSettings).lineStrokeStyle)
.foregroundColor(self.colorPerLineSegment!(dataPoints[index]))


}


}
}

}.mask(lineAnimationMaskingView(width:geo.size.width))

}
}

}

}

/// for line drawing animation if line composed of several paths in separate colours
private func lineAnimationMaskingView(width: CGFloat)->some View {
HStack {
Rectangle().fill(Color.white.opacity(0.5)).frame(width: self.showLineSegments ? width : 0, alignment: .trailing)
Spacer()
}
}

private func gradient() -> some View {
settings.gradient
.padding(.bottom, 1)
Expand All @@ -263,7 +273,7 @@ public struct DYLineChartView: View, DYGridChart {
func pathFor(width: CGFloat, height: CGFloat, closeShape: Bool)->Path {
Path { path in

path = self.drawLineWith(path: &path, height: height, width: width)
path = self.drawCompletePathWith(path: &path, height: height, width: width)

// Finally close the subpath off by looping around to the beginning point.
if closeShape {
Expand All @@ -273,54 +283,38 @@ public struct DYLineChartView: View, DYGridChart {
}
}
}
/// new
func drawLineWith(path: inout Path, index: Int, height: CGFloat, width: CGFloat) -> Path {

///
func drawPathWith(path: inout Path, index: Int, height: CGFloat, width: CGFloat) -> Path {

let mappedYValue0 = self.convertToYCoordinate(value: dataPoints[index].yValue, height: height)
let mappedXValue0 = self.convertToXCoordinate(value: dataPoints[index].xValue, width: width)
let point0 = CGPoint(x: settings.lateralPadding.leading + mappedXValue0, y: height - mappedYValue0)
path.move(to: point0)
if index < self.dataPoints.count - 1 {
let nextIndex = index + 1
let mappedYValue1 = self.convertToYCoordinate(value: dataPoints[nextIndex].yValue, height: height)
let mappedXValue1 = self.convertToXCoordinate(value: dataPoints[nextIndex].xValue, width: width)
let point1 = CGPoint(x: settings.lateralPadding.leading + mappedXValue1, y: height - mappedYValue1)
let midPoint = CGPoint.midPointForPoints(p1: point0, p2: point1)

path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: point0))
path.addQuadCurve(to: point1, control: CGPoint.controlPointForPoints(p1: midPoint, p2: point1))
path.addLine(to: point1)


_ = self.connectPointsWith(path: &path, index: nextIndex, point0: point0, height: height, width: width)

}
// point1 = point2

// }


return path

}

func drawLineWith(path: inout Path, height: CGFloat, width: CGFloat)->Path {
func drawCompletePathWith(path: inout Path, height: CGFloat, width: CGFloat)->Path {

guard let firstYValue = dataPoints.first?.yValue else {return path}

var point1 = CGPoint(x: settings.lateralPadding.leading, y: height - self.convertToYCoordinate(value: firstYValue, height: height))
path.move(to: point1)
var index:CGFloat = 0
var point0 = CGPoint(x: settings.lateralPadding.leading, y: height - self.convertToYCoordinate(value: firstYValue, height: height))
path.move(to: point0)
var index:Int = 0

for _ in dataPoints {
if index != 0 {

let mappedYValue = self.convertToYCoordinate(value: dataPoints[Int(index)].yValue, height: height)
let mappedXValue = self.convertToXCoordinate(value: dataPoints[Int(index)].xValue, width: width)
let point2 = CGPoint(x: settings.lateralPadding.leading + mappedXValue, y: height - mappedYValue)
let midPoint = CGPoint.midPointForPoints(p1: point1, p2: point2)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: point1))
path.addQuadCurve(to: point2, control: CGPoint.controlPointForPoints(p1: midPoint, p2: point2))
path.addLine(to: point2)
point1 = point2
point0 = self.connectPointsWith(path: &path, index: index, point0: point0, height: height, width: width)

}
index += 1

Expand All @@ -330,6 +324,18 @@ public struct DYLineChartView: View, DYGridChart {

}

private func connectPointsWith(path: inout Path, index: Int, point0: CGPoint, height: CGFloat, width: CGFloat)->CGPoint {

let mappedYValue = self.convertToYCoordinate(value: dataPoints[index].yValue, height: height)
let mappedXValue = self.convertToXCoordinate(value: dataPoints[index].xValue, width: width)
let point1 = CGPoint(x: settings.lateralPadding.leading + mappedXValue, y: height - mappedYValue)
let midPoint = CGPoint.midPointForPoints(p1: point0, p2: point1)
path.addQuadCurve(to: midPoint, control: CGPoint.controlPointForPoints(p1: midPoint, p2: point0))
path.addQuadCurve(to: point1, control: CGPoint.controlPointForPoints(p1: midPoint, p2: point1))
path.addLine(to: point1)
return point1
}

private func points()->some View {
GeometryReader { geo in

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct BasicBarChartExample: View {
return Date(timeIntervalSinceReferenceDate: xValue).toString(format:"dd-MM")
}, yValueConverter: { (yValue) -> String in
return yValue.toDecimalString(maxFractionDigits: 0)
}, gradientPerBar: nil, chartFrameHeight: proxy.size.height > proxy.size.width ? proxy.size.height * 0.4 : proxy.size.height * 0.65, settings: DYBarChartSettings(yAxisSettings: YAxisSettings(yAxisPosition: .trailing, yAxisFontSize: fontSize, yAxisMinMaxOverride: (min:0, max:nil)), xAxisSettings: DYBarChartXAxisSettings(showXAxis: true, xAxisFontSize: fontSize)))
}, gradientPerBar: nil, chartFrameHeight: proxy.size.height > proxy.size.width ? proxy.size.height * 0.4 : proxy.size.height * 0.65, settings: DYBarChartSettings(selectionIndicatorColor: .green, selectedBarGradient: LinearGradient(colors: [.green, .green.opacity(0.8)], startPoint: .top, endPoint: .bottom) , yAxisSettings: YAxisSettings(yAxisPosition: .trailing, yAxisFontSize: fontSize, yAxisMinMaxOverride: (min:0, max:nil)), xAxisSettings: DYBarChartXAxisSettings(showXAxis: true, xAxisFontSize: fontSize)))

Spacer()
}.padding()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ struct CustomYAxisIntervalExampleLineChart: View {

}, yValueConverter: { (yValue) -> String in
return TimeInterval(yValue).toString() ?? ""
}, colorPerPoint: {(dataPoint) in return dataPointSegmentColor(dataPoint: dataPoint)}, colorPerLineSegment: {(dataPoint) in dataPointSegmentColor(dataPoint: dataPoint)}, chartFrameHeight: proxy.size.height > proxy.size.width ? proxy.size.height * 0.4 : proxy.size.height * 0.65, settings: DYLineChartSettings(lineColor: .blue, gradient: LinearGradient(gradient: Gradient(colors: [.blue, Color.white]), startPoint: .top, endPoint: .bottom), pointColor: .blue, selectorLineColor: .blue, selectorLinePointColor: .blue, yAxisSettings: YAxisSettings(showYAxis: true, yAxisPosition: .trailing, yAxisViewWidth: self.yAxisWidth, yAxisFontSize: fontSize, yAxisMinMaxOverride: (min: 0, max: Double(Int(exampleData.map({$0.yValue}).max() ?? 0).nearest(multipleOf: 1800, up: true))), yAxisIntervalOverride: 1800), xAxisSettings: DYLineChartXAxisSettings(showXAxis: true, xAxisInterval: 604800, xAxisFontSize: fontSize))) // 604800 seconds per week
}, colorPerPoint: {(dataPoint) in return dataPointSegmentColor(dataPoint: dataPoint)}, colorPerLineSegment: {(dataPoint) in dataPointSegmentColor(dataPoint: dataPoint)}, chartFrameHeight: proxy.size.height > proxy.size.width ? proxy.size.height * 0.4 : proxy.size.height * 0.65, settings: DYLineChartSettings(lineColor: .blue, showGradient: true, gradient: LinearGradient(gradient: Gradient(colors: [.blue, Color.white]), startPoint: .top, endPoint: .bottom), pointColor: .blue, selectorLineColor: .blue, selectorLinePointColor: .blue, yAxisSettings: YAxisSettings(showYAxis: true, yAxisPosition: .trailing, yAxisViewWidth: self.yAxisWidth, yAxisFontSize: fontSize, yAxisMinMaxOverride: (min: 0, max: Double(Int(exampleData.map({$0.yValue}).max() ?? 0).nearest(multipleOf: 1800, up: true))), yAxisIntervalOverride: 1800), xAxisSettings: DYLineChartXAxisSettings(showXAxis: true, xAxisInterval: 604800, xAxisFontSize: fontSize))) // 604800 seconds per week
Spacer()
}.padding()
.navigationTitle("Workout Time Per Week")
Expand Down

0 comments on commit c0c42e6

Please sign in to comment.