Channel statistics improvements

This commit is contained in:
Ilya Laktyushin
2020-03-12 05:01:21 +04:00
parent d8b99880ea
commit f5dbf47b68
154 changed files with 6186 additions and 10908 deletions

View File

@@ -0,0 +1,94 @@
//
// ChartVisibilityItem.swift
// GraphCore
//
// Created by Mikhail Filimonov on 26.02.2020.
// Copyright © 2020 Telegram. All rights reserved.
//
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
struct ChartVisibilityItem {
var title: String
var color: GColor
static func generateItemsFrames(for chartWidth: CGFloat, items: [ChartVisibilityItem]) -> [CGRect] {
var previousPoint = CGPoint(x: ChatVisibilityItemConstants.insets.left, y: ChatVisibilityItemConstants.insets.top)
var frames: [CGRect] = []
for item in items {
let labelSize = textSize(with: item.title, font: ChatVisibilityItemConstants.textFont)
let width = (labelSize.width + ChatVisibilityItemConstants.labelTextApproxInsets).rounded(.up)
if previousPoint.x + width < (chartWidth - ChatVisibilityItemConstants.insets.left - ChatVisibilityItemConstants.insets.right) {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: ChatVisibilityItemConstants.itemHeight)))
} else if previousPoint.x <= ChatVisibilityItemConstants.insets.left {
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: ChatVisibilityItemConstants.itemHeight)))
} else {
previousPoint.y += ChatVisibilityItemConstants.itemHeight + ChatVisibilityItemConstants.itemSpacing
previousPoint.x = ChatVisibilityItemConstants.insets.left
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: ChatVisibilityItemConstants.itemHeight)))
}
previousPoint.x += width + ChatVisibilityItemConstants.itemSpacing
}
return frames
}
}
enum ChatVisibilityItemConstants {
static let itemHeight: CGFloat = 30
static let itemSpacing: CGFloat = 8
static let labelTextApproxInsets: CGFloat = 40
static let insets = NSEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
static let textFont = NSFont.systemFont(ofSize: 14, weight: .medium)
}
public struct ChartDetailsViewModel {
public struct Value {
public let prefix: String?
public let title: String
public let value: String
public let color: GColor
public let visible: Bool
public init(prefix: String?,
title: String,
value: String,
color: GColor,
visible: Bool) {
self.prefix = prefix
self.title = title
self.value = value
self.color = color
self.visible = visible
}
}
public internal(set) var title: String
public internal(set) var showArrow: Bool
public internal(set) var showPrefixes: Bool
public internal(set) var values: [Value]
public internal(set) var totalValue: Value?
public internal(set) var tapAction: (() -> Void)?
static let blank = ChartDetailsViewModel(title: "", showArrow: false, showPrefixes: false, values: [], totalValue: nil, tapAction: nil)
public init(title: String,
showArrow: Bool,
showPrefixes: Bool,
values: [Value],
totalValue: Value?,
tapAction: (() -> Void)?) {
self.title = title
self.showArrow = showArrow
self.showPrefixes = showPrefixes
self.values = values
self.totalValue = totalValue
self.tapAction = tapAction
}
}

View File

@@ -0,0 +1,103 @@
//
// ChardData.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
public struct ChartsCollection {
public struct Chart {
public internal(set) var color: GColor
public internal(set) var name: String
public internal(set) var values: [Double]
}
public internal(set) var axisValues: [Date]
public internal(set) var chartValues: [Chart]
static let blank = ChartsCollection(axisValues: [], chartValues: [])
var isBlank: Bool {
return axisValues.isEmpty || chartValues.isEmpty
}
}
public extension ChartsCollection {
public init(from decodedData: [String: Any]) throws {
guard let columns = decodedData["columns"] as? [[Any]] else {
throw ChartsError.generalConversion("Unable to get columns from: \(decodedData)")
}
guard let types = decodedData["types"] as? [String: String] else {
throw ChartsError.generalConversion("Unable to get types from: \(decodedData)")
}
guard let names = decodedData["names"] as? [String: String] else {
throw ChartsError.generalConversion("Unable to get names from: \(decodedData)")
}
guard let colors = decodedData["colors"] as? [String: String] else {
throw ChartsError.generalConversion("Unable to get colors from: \(decodedData)")
}
// chart.colors Color for each variable in 6-hex-digit format (e.g. "#AAAAAA").
// chart.names Name for each variable.
// chart.percentage true for percentage based values.
// chart.stacked true for values stacking on top of each other.
// chart.y_scaled true for charts with 2 Y axes.
var axixValuesToSetup: [Date] = []
var chartToSetup: [Chart] = []
for column in columns {
guard let columnId = column.first as? String else {
throw ChartsError.generalConversion("Unable to get column name from: \(column)")
}
guard let typeString = types[columnId], let type = ColumnType(rawValue: typeString) else {
throw ChartsError.generalConversion("Unable to get column type from: \(types) - \(columnId)")
}
switch type {
case .axix:
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
case .chart, .bar, .area, .step:
guard let colorString = colors[columnId],
let color = GColor(hexString: colorString) else {
throw ChartsError.generalConversion("Unable to get color name from: \(colors) - \(columnId)")
}
guard let name = names[columnId] else {
throw ChartsError.generalConversion("Unable to get column name from: \(names) - \(columnId)")
}
let values = try column.dropFirst().map { try Convert.doubleFrom($0) }
chartToSetup.append(Chart(color: color,
name: name,
values: values))
}
}
guard axixValuesToSetup.isEmpty == false,
chartToSetup.isEmpty == false,
chartToSetup.firstIndex(where: { $0.values.count != axixValuesToSetup.count }) == nil else {
throw ChartsError.generalConversion("Saniazing: Invalid number of items: \(axixValuesToSetup), \(chartToSetup)")
}
self.axisValues = axixValuesToSetup
self.chartValues = chartToSetup
}
}
private enum ColumnType: String {
case axix = "x"
case chart = "line"
case area = "area"
case bar = "bar"
case step = "step"
}

View File

@@ -0,0 +1,196 @@
//
// ChartsDataManager.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
public class ChartsDataManager {
public static func readChart(item: [String: Any], extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
var collection = try ChartsCollection(from: item)
for _ in 0..<extraCopiesCount {
for valueIndex in collection.chartValues.indices {
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
}
guard let firstValue = collection.axisValues.first,
let lastValule = collection.axisValues.last else {
throw ChartsError.invalidJson
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collection.axisValues.indices {
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collection.axisValues.append(newDate)
}
}
if sync {
success(collection)
} else {
DispatchQueue.main.async {
success(collection)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
public static func readChart(data: Data, extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let item = decoded as? [String: Any] else {
throw ChartsError.invalidJson
}
var collection = try ChartsCollection(from: item)
for _ in 0..<extraCopiesCount {
for valueIndex in collection.chartValues.indices {
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
}
guard let firstValue = collection.axisValues.first,
let lastValule = collection.axisValues.last else {
throw ChartsError.invalidJson
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collection.axisValues.indices {
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collection.axisValues.append(newDate)
}
}
if sync {
success(collection)
} else {
DispatchQueue.main.async {
success(collection)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
public static func readChart(file: URL, extraCopiesCount: Int = 0, sync: Bool, success: @escaping (ChartsCollection) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
let data = try Data(contentsOf: file)
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let item = decoded as? [String: Any] else {
throw ChartsError.invalidJson
}
var collection = try ChartsCollection(from: item)
for _ in 0..<extraCopiesCount {
for valueIndex in collection.chartValues.indices {
collection.chartValues[valueIndex] .values += collection.chartValues[valueIndex].values
}
guard let firstValue = collection.axisValues.first,
let lastValule = collection.axisValues.last else {
throw ChartsError.invalidJson
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collection.axisValues.indices {
let intervalToAdd = collection.axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collection.axisValues.append(newDate)
}
}
if sync {
success(collection)
} else {
DispatchQueue.main.async {
success(collection)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
public static func readCharts(file: URL, extraCopiesCount: Int = 0, sync: Bool, success: @escaping ([ChartsCollection]) -> Void, failure: @escaping (Error) -> Void) {
let workItem: (() -> Void) = {
do {
let data = try Data(contentsOf: file)
let decoded = try JSONSerialization.jsonObject(with: data, options: [])
guard let items = decoded as? [[String: Any]] else {
throw ChartsError.invalidJson
}
var collections = try items.map { try ChartsCollection(from: $0) }
for _ in 0..<extraCopiesCount {
for collrctionIndex in collections.indices {
for valueIndex in collections[collrctionIndex].chartValues.indices {
collections[collrctionIndex].chartValues[valueIndex] .values += collections[collrctionIndex].chartValues[valueIndex].values
}
guard let firstValue = collections[collrctionIndex].axisValues.first,
let lastValule = collections[collrctionIndex].axisValues.last else {
return
}
let startItem = lastValule.addingTimeInterval(.day)
for valueIndex in collections[collrctionIndex].axisValues.indices {
let intervalToAdd = collections[collrctionIndex].axisValues[valueIndex].timeIntervalSince(firstValue)
let newDate = startItem.addingTimeInterval(intervalToAdd)
collections[collrctionIndex].axisValues.append(newDate)
}
}
}
if sync {
success(collections)
} else {
DispatchQueue.main.async {
success(collections)
}
}
} catch {
DispatchQueue.main.async {
assertionFailure("Error occure: \(error)")
failure(error)
}
}
}
if sync {
workItem()
} else {
DispatchQueue.global().async(execute: workItem)
}
}
}

View File

@@ -0,0 +1,19 @@
//
// ChartsError.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
enum ChartsError: Error {
case invalidJson
case generalConversion(String)
}

View File

@@ -0,0 +1,47 @@
//
// Convert.swift
// GraphTest
//
// Created by Andrei Salavei on 3/11/19.
// Copyright © 2019 Andrei Salavei. All rights reserved.
//
import Foundation
#if os(macOS)
import Cocoa
#else
import UIKit
#endif
public enum Convert {
public static func doubleFrom(_ value: Any?) throws -> Double {
guard let double = try doubleFrom(value, lenientCast: false) else {
throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)")
}
return double
}
public static func doubleFrom(_ value: Any?, lenientCast: Bool = false) throws -> Double? {
guard let value = value else {
return nil
}
if let intValue = value as? Int {
return Double(intValue)
} else if let floatValue = value as? Float {
return Double(floatValue)
} else if let int64Value = value as? Int64 {
return Double(int64Value)
} else if let intValue = value as? Int {
return Double(intValue)
} else if let stringValue = value as? String {
if let doubleValue = Double(stringValue) {
return doubleValue
}
}
if lenientCast {
return nil
} else {
throw ChartsError.generalConversion("Unable to cast \(String(describing: value)) to \(Double.self)")
}
}
}