mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
200 lines
8.9 KiB
Swift
200 lines
8.9 KiB
Swift
//
|
|
// PieChartRenderer.swift
|
|
// GraphTest
|
|
//
|
|
// Created by Andrei Salavei on 4/11/19.
|
|
// Copyright © 2019 Andrei Salavei. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
#if os(macOS)
|
|
import Cocoa
|
|
#else
|
|
import UIKit
|
|
#endif
|
|
|
|
class PieChartRenderer: BaseChartRenderer {
|
|
struct PieComponent: Hashable {
|
|
var color: GColor
|
|
var value: CGFloat
|
|
}
|
|
|
|
override func setup(verticalRange: ClosedRange<CGFloat>, animated: Bool, timeFunction: TimeFunction? = nil) {
|
|
super.setup(verticalRange: 0...1, animated: animated, timeFunction: timeFunction)
|
|
}
|
|
|
|
var valuesFormatter: NumberFormatter = NumberFormatter()
|
|
var drawValues: Bool = true
|
|
|
|
private var componentsAnimators: [AnimationController<CGFloat>] = []
|
|
private lazy var transitionAnimator: AnimationController<CGFloat> = { AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }()
|
|
private var oldPercentageData: [PieComponent] = []
|
|
private var percentageData: [PieComponent] = []
|
|
private var setlectedSegmentsAnimators: [AnimationController<CGFloat>] = []
|
|
|
|
var drawPie: Bool = true
|
|
var initialAngle: CGFloat = .pi / 3
|
|
var hasSelectedSegments: Bool {
|
|
return selectedSegment != nil
|
|
}
|
|
private(set) var selectedSegment: Int?
|
|
func selectSegmentAt(at indexToSelect: Int?, animated: Bool) {
|
|
guard selectedSegment != indexToSelect else {
|
|
return
|
|
}
|
|
selectedSegment = indexToSelect
|
|
for (index, animator) in setlectedSegmentsAnimators.enumerated() {
|
|
let fraction: CGFloat = (index == indexToSelect) ? 1.0 : 0.0
|
|
if animated {
|
|
animator.animate(to: fraction, duration: .defaultDuration / 2)
|
|
} else {
|
|
animator.set(current: fraction)
|
|
}
|
|
}
|
|
}
|
|
|
|
func updatePercentageData(_ percentageData: [PieComponent], animated: Bool) {
|
|
if self.percentageData.count != percentageData.count {
|
|
componentsAnimators = percentageData.map { _ in AnimationController<CGFloat>(current: 1, refreshClosure: self.refreshClosure) }
|
|
setlectedSegmentsAnimators = percentageData.map { _ in AnimationController<CGFloat>(current: 0, refreshClosure: self.refreshClosure) }
|
|
}
|
|
if animated {
|
|
self.oldPercentageData = self.currentTransitionAnimationData
|
|
self.percentageData = percentageData
|
|
transitionAnimator.completionClosure = { [weak self] in
|
|
self?.oldPercentageData = []
|
|
}
|
|
transitionAnimator.set(current: 0)
|
|
transitionAnimator.animate(to: 1, duration: .defaultDuration)
|
|
} else {
|
|
self.oldPercentageData = []
|
|
self.percentageData = percentageData
|
|
transitionAnimator.set(current: 0)
|
|
}
|
|
}
|
|
|
|
func setComponentVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
|
componentsAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
|
}
|
|
|
|
var lastRenderedBounds: CGRect = .zero
|
|
var lastRenderedChartFrame: CGRect = .zero
|
|
func selectedItemIndex(at point: CGPoint) -> Int? {
|
|
let touchPosition = lastRenderedChartFrame.origin + point * lastRenderedChartFrame.size
|
|
let center = CGPoint(x: lastRenderedChartFrame.midX, y: lastRenderedChartFrame.midY)
|
|
let radius = min(lastRenderedChartFrame.width, lastRenderedChartFrame.height) / 2
|
|
if center.distanceTo(touchPosition) > radius { return nil }
|
|
let angle = (center - touchPosition).angle + .pi
|
|
let currentData = currentlyVisibleData
|
|
let total: CGFloat = currentData.map({ $0.value }).reduce(0, +)
|
|
var startAngle: CGFloat = initialAngle
|
|
for (index, piece) in currentData.enumerated() {
|
|
let percent = piece.value / total
|
|
let segmentSize = 2 * .pi * percent
|
|
let endAngle = startAngle + segmentSize
|
|
if angle >= startAngle && angle <= endAngle ||
|
|
angle + .pi * 2 >= startAngle && angle + .pi * 2 <= endAngle {
|
|
return index
|
|
}
|
|
startAngle = endAngle
|
|
}
|
|
return nil
|
|
}
|
|
|
|
private var currentTransitionAnimationData: [PieComponent] {
|
|
if transitionAnimator.isAnimating {
|
|
let animationFraction = transitionAnimator.current
|
|
return percentageData.enumerated().map { arg in
|
|
return PieComponent(color: arg.element.color,
|
|
value: oldPercentageData[arg.offset].value * (1 - animationFraction) + arg.element.value * animationFraction)
|
|
}
|
|
} else {
|
|
return percentageData
|
|
}
|
|
}
|
|
|
|
var currentlyVisibleData: [PieComponent] {
|
|
return currentTransitionAnimationData.enumerated().map { arg in
|
|
return PieComponent(color: arg.element.color,
|
|
value: arg.element.value * componentsAnimators[arg.offset].current)
|
|
}
|
|
}
|
|
|
|
override func render(context: CGContext, bounds: CGRect, chartFrame: CGRect) {
|
|
guard isEnabled && verticalRange.current.distance > 0 && verticalRange.current.distance > 0 else { return }
|
|
lastRenderedBounds = bounds
|
|
lastRenderedChartFrame = chartFrame
|
|
let chartAlpha = chartAlphaAnimator.current
|
|
if chartAlpha == 0 { return }
|
|
|
|
let center = CGPoint(x: chartFrame.midX, y: chartFrame.midY)
|
|
let radius = min(chartFrame.width, chartFrame.height) / 2
|
|
|
|
let currentData = currentlyVisibleData
|
|
|
|
let total: CGFloat = currentData.map({ $0.value }).reduce(0, +)
|
|
guard total > 0 else {
|
|
return
|
|
}
|
|
|
|
let animationSelectionOffset: CGFloat = radius / 15
|
|
let maximumFontSize: CGFloat = radius / 7
|
|
let minimumFontSize: CGFloat = 4
|
|
let centerOffsetStartAngle = CGFloat.pi / 4
|
|
let minimumValueToDraw: CGFloat = 0.015
|
|
let diagramRadius = radius - animationSelectionOffset
|
|
|
|
let numberOfVisibleItems = currentlyVisibleData.filter { $0.value > 0 }.count
|
|
var startAngle: CGFloat = initialAngle
|
|
for (index, piece) in currentData.enumerated() {
|
|
let percent = piece.value / total
|
|
guard percent > 0 else { continue }
|
|
let segmentSize = 2 * .pi * percent * chartAlpha
|
|
let endAngle = startAngle + segmentSize
|
|
let centerAngle = (startAngle + endAngle) / 2
|
|
let labelVector = CGPoint(x: cos(centerAngle),
|
|
y: sin(centerAngle))
|
|
|
|
let selectionAnimationFraction = (numberOfVisibleItems > 1 ? setlectedSegmentsAnimators[index].current : 0)
|
|
|
|
let updatedCenter = CGPoint(x: center.x + labelVector.x * selectionAnimationFraction * animationSelectionOffset,
|
|
y: center.y + labelVector.y * selectionAnimationFraction * animationSelectionOffset)
|
|
if drawPie {
|
|
context.saveGState()
|
|
context.setFillColor(piece.color.withAlphaComponent(piece.color.alphaValue * chartAlpha).cgColor)
|
|
context.move(to: updatedCenter)
|
|
context.addArc(center: updatedCenter,
|
|
radius: radius - animationSelectionOffset,
|
|
startAngle: startAngle,
|
|
endAngle: endAngle,
|
|
clockwise: false)
|
|
context.fillPath()
|
|
context.restoreGState()
|
|
}
|
|
|
|
if drawValues && percent >= minimumValueToDraw {
|
|
context.saveGState()
|
|
|
|
let text = valuesFormatter.string(from: percent * 100)
|
|
let fraction = crop(0, segmentSize / centerOffsetStartAngle, 1)
|
|
let fontSize = (minimumFontSize + (maximumFontSize - minimumFontSize) * fraction).rounded(.up)
|
|
let labelPotisionOffset = diagramRadius / 2 + diagramRadius / 2 * (1 - fraction)
|
|
let font = NSFont.systemFont(ofSize: fontSize, weight: .bold)
|
|
let labelsEaseInColor = crop(0, chartAlpha * chartAlpha * 2 - 1, 1)
|
|
let attributes: [NSAttributedString.Key: Any] = [.foregroundColor: GColor.white.withAlphaComponent(labelsEaseInColor),
|
|
.font: font]
|
|
|
|
let attributedString = NSAttributedString(string: text, attributes: attributes)
|
|
let textNode = LabelNode.layoutText(attributedString, bounds.size)
|
|
|
|
let labelPoint = CGPoint(x: labelVector.x * labelPotisionOffset + updatedCenter.x - textNode.0.size.width / 2,
|
|
y: labelVector.y * labelPotisionOffset + updatedCenter.y - textNode.0.size.height / 2)
|
|
textNode.1.draw(CGRect(origin: labelPoint, size: textNode.0.size), in: context, backingScaleFactor: deviceScale)
|
|
context.restoreGState()
|
|
}
|
|
|
|
startAngle = endAngle
|
|
}
|
|
}
|
|
}
|