diff --git a/ios/ColorParser.swift b/ios/ColorParser.swift index b9d8f2d4..663e40bb 100644 --- a/ios/ColorParser.swift +++ b/ios/ColorParser.swift @@ -26,6 +26,9 @@ class ColorParser { "teal": .systemTeal] func fromCSS(cssString: String) -> UIColor { + if(cssString == "none") { + return UIColor.clear + } let hex = cssString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted) if cssString.hasPrefix("#") { var int = UInt64() diff --git a/ios/DataCellView.swift b/ios/DataCellView.swift index f29eb0eb..c3e9d7db 100644 --- a/ios/DataCellView.swift +++ b/ios/DataCellView.swift @@ -33,20 +33,31 @@ class DataCellView: UICollectionViewCell { let views = contentView.subviews row.cells.enumerated().forEach {(index, element) in let col = cols[index] - - if let label = views[index] as? PaddedLabel { - let newFrame = CGRect(x: x, y: 0, width: Int(col.width!), height: theme.rowHeight! * numberOfLines) - label.textAlignment = element.qNum == nil ? .left : .right - x += Int(col.width!) - label.frame = newFrame.integral - label.center = CGPoint(x: floor(label.center.x), y: floor(label.center.y)) - label.text = element.qText - label.column = index - label.cell = element - label.checkSelected(selectionsEngine) - label.textColor = cellColor! - label.numberOfLines = numberOfLines + let newFrame = CGRect(x: x, y: 0, width: Int(col.width!), height: theme.rowHeight! * numberOfLines) + if let representation = col.representation { + if representation.type == "miniChart" { + if let miniChart = views[index] as? MiniChartView { + miniChart.frame = newFrame.integral + miniChart.setChartData(data: element, representedAs: representation) + } + } + else { + if let label = views[index] as? PaddedLabel { + + label.textAlignment = element.qNum == nil ? .left : .right + label.frame = newFrame.integral + label.center = CGPoint(x: floor(label.center.x), y: floor(label.center.y)) + label.text = element.qText + label.column = index + label.cell = element + label.checkSelected(selectionsEngine) + label.textColor = cellColor! + label.numberOfLines = numberOfLines + } + } } + x += Int(col.width!) + } } @@ -56,16 +67,26 @@ class DataCellView: UICollectionViewCell { var x = 0 clearAllCells() for col in cols { - let label = PaddedLabel(frame: .zero) - if col.isDim == true { - if let selectionsEngine = selectionsEngine { - label.makeSelectable(selectionsEngine: selectionsEngine) + + if let representation = col.representation { + if(representation.type == "miniChart") { + let miniChartView = MiniChartView(frame: .zero) + contentView.addSubview(miniChartView) + } + else { + let label = PaddedLabel(frame: .zero) + if col.isDim == true { + if let selectionsEngine = selectionsEngine { + label.makeSelectable(selectionsEngine: selectionsEngine) + } + } + let sizedFont = UIFont.systemFont(ofSize: 14) + label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: sizedFont) + label.adjustsFontForContentSizeCategory = true + contentView.addSubview(label) } } - let sizedFont = UIFont.systemFont(ofSize: 14) - label.font = UIFontMetrics(forTextStyle: .body).scaledFont(for: sizedFont) - label.adjustsFontForContentSizeCategory = true - contentView.addSubview(label) + x += Int(col.width!) } } @@ -93,19 +114,22 @@ class DataCellView: UICollectionViewCell { } let new = CGRect(x: old.origin.x + translation.x, y: 0, width: newNeighbourWidth, height: old.height) v.frame = new + v.setNeedsDisplay() } - resizeLabel(view: view, width: newWidth) - + resizeContentView(view: view, width: newWidth) + return true } - fileprivate func resizeLabel(view: UIView, width: CGFloat) { + fileprivate func resizeContentView(view: UIView, width: CGFloat) { if view.frame.width != width { let oldFrame = view.frame let newFrame = CGRect(x: oldFrame.origin.x, y: 0, width: width, height: oldFrame.height) view.frame = newFrame + view.setNeedsDisplay() } + } override func draw(_ rect: CGRect) { diff --git a/ios/DataColumn.swift b/ios/DataColumn.swift index 0191deff..b390167d 100644 --- a/ios/DataColumn.swift +++ b/ios/DataColumn.swift @@ -6,6 +6,41 @@ // import Foundation + +struct MiniChartColor: Codable { + let color: String? + let index: Int? +} + +struct ChartColors: Codable { + let first: MiniChartColor? + let last: MiniChartColor? + let min: MiniChartColor? + let max: MiniChartColor? + let negative: MiniChartColor? + let positive: MiniChartColor? + let main: MiniChartColor? +} + +struct YAxis: Codable { + let position: String? + let scale: String? +} + +struct MiniChart: Codable { + let type: String? + let colors: ChartColors? + let showDots: Bool? + let yAxis: YAxis? +} + +struct Representation: Codable { + let type: String? + let miniChart: MiniChart? + let globalMax: Double? + let globalMin: Double? +} + struct DataColumn: Codable { var isDim: Bool = false var active: Bool = false @@ -15,6 +50,9 @@ struct DataColumn: Codable { let align: String? let sortDirection: String? let dataColIdx: Double? + let representation: Representation? + let max: Double? + let min: Double? } struct TotalsCell: Decodable { diff --git a/ios/DataRow.swift b/ios/DataRow.swift index 73ce1bc8..1e02fdcc 100644 --- a/ios/DataRow.swift +++ b/ios/DataRow.swift @@ -6,6 +6,16 @@ // import Foundation +struct MatrixCell: Decodable { + let qNum: Double? +} + +struct Matrix: Decodable { + let qMatrix: [[MatrixCell]]? + let qMax: Double? + let qMin: Double? +} + struct DataCell: Decodable { var qText: String? var qNum: Double? @@ -16,6 +26,8 @@ struct DataCell: Decodable { var isDim: Bool? var rawRowIdx: Double? var rawColIdx: Double? + var qMiniChart: Matrix? + enum CodingKeys: String, CodingKey { case qText case qNum @@ -26,6 +38,7 @@ struct DataCell: Decodable { case isDim case rawRowIdx case rawColIdx + case qMiniChart } init(from decoder: Decoder) throws { @@ -38,6 +51,7 @@ struct DataCell: Decodable { self.rawRowIdx = try container.decodeIfPresent(Double.self, forKey: .rawRowIdx) ?? -1 self.rawColIdx = try container.decodeIfPresent(Double.self, forKey: .rawColIdx) ?? -1 self.isDim = try container.decodeIfPresent(Bool.self, forKey: .isDim) ?? false + self.qMiniChart = try container.decodeIfPresent(Matrix.self, forKey: .qMiniChart) ?? nil if let temp = try? container.decode(Double.self, forKey: .qNum) { self.qNum = temp diff --git a/ios/MiniBarChart.swift b/ios/MiniBarChart.swift new file mode 100644 index 00000000..6aa179b5 --- /dev/null +++ b/ios/MiniBarChart.swift @@ -0,0 +1,33 @@ +// +// MiniBarChart.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-03. +// + +import Foundation +class MiniBarChart : MiniChartRenderer { + + override func render(_ ctx: CGContext, rect: CGRect) { + guard let data = data else {return} + guard let rows = data.qMatrix else {return} + if (rect.size.height == 0) {return} + ctx.clear(rect) + getBandWidth(rect: rect, data: data) + getScale(rect:rect, data:data) + var x = padding + horizontalPadding / 2; + var index = 0 + for row in rows { + mainColor.set() + let value = row[1].qNum ?? 1.0 + let height = value * scale; + let y = rect.height - height - verticalPadding / 2 + let rect = CGRect(x: x, y: y, width: bandWidth, height:height) + setColor(index, value: value, count: rows.count) + ctx.fill(rect) + x += padding * 2 + bandWidth + index += 1 + } + } + +} diff --git a/ios/MiniChartRenderer.swift b/ios/MiniChartRenderer.swift new file mode 100644 index 00000000..93cdf2c6 --- /dev/null +++ b/ios/MiniChartRenderer.swift @@ -0,0 +1,87 @@ +// +// MiniChart.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-03. +// + +import Foundation +class MiniChartRenderer { + var data: Matrix? + var representation: Representation? + var firstColor = UIColor.systemBlue + var lastColor = UIColor.systemBlue + var mainColor = UIColor.systemBlue + var positiveColor = UIColor.systemBlue + var negativeColor = UIColor.systemBlue + var maxColor = UIColor.systemBlue + var minColor = UIColor.systemBlue + var bandWidth = 0.0 + var padding = 0.0 + var maxValue = Double.infinity + var minValue = -Double.infinity + var globalMaxValue = Double.infinity + var globalMinValue = -Double.infinity + var yScale = Double.infinity + var scale = 0.0 + var verticalPadding = 0.0 + var horizontalPadding = 0.0 + var showDots = false + + init() { + + } + + init(rep: Representation) { + self.representation = rep + mainColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.main?.color ?? "black") + firstColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.first?.color ?? "black") + lastColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.last?.color ?? "black") + positiveColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.positive?.color ?? "black") + negativeColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.negative?.color ?? "black") + maxColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.max?.color ?? "black") + minColor = ColorParser().fromCSS(cssString: rep.miniChart?.colors?.min?.color ?? "black") + showDots = rep.miniChart?.showDots ?? false + } + + func setColor(_ index: Int, value: Double, count: Int) { + if(value == maxValue && maxColor != .clear) { + maxColor.set() + return + } + + if(value == minValue && minColor != .clear) { + minColor.set() + return + } + + if(index == 0 && firstColor != .clear) { + firstColor.set() + return + } + + if(index == count - 1 && lastColor != .clear) { + lastColor.set() + return; + } + + mainColor.set() + } + + func getBandWidth(rect: CGRect, data: Matrix) { + let count = data.qMatrix?.count ?? 1 + horizontalPadding = rect.width * 0.05; + let width = rect.width * 0.95; + let totalBandWidth = width / CGFloat(count) + bandWidth = totalBandWidth * 0.8 + padding = totalBandWidth * 0.1 + } + + func getScale(rect: CGRect, data: Matrix) { + let height = rect.height - 8; + verticalPadding = 8 + scale = height / yScale + } + + func render(_ ctx: CGContext, rect: CGRect){} +} diff --git a/ios/MiniChartView.swift b/ios/MiniChartView.swift new file mode 100644 index 00000000..e5586f94 --- /dev/null +++ b/ios/MiniChartView.swift @@ -0,0 +1,41 @@ +// +// MiniChartView.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-03. +// + +import Foundation +import QuartzCore + +class MiniChartView : UIView { + var miniChart = MiniChartRenderer() + + func setChartData(data: DataCell, representedAs rep: Representation) { + self.backgroundColor = UIColor.clear + self.layer.contentsScale = UIScreen.main.scale + if rep.miniChart?.type == "bars" { + miniChart = MiniBarChart(rep: rep) + } else if(rep.miniChart?.type == "sparkline") { + miniChart = MiniSparkLineChart(rep: rep) + } else if(rep.miniChart?.type == "dots") { + miniChart = MiniDotGraph(rep: rep) + } else if(rep.miniChart?.type == "posNeg") { + miniChart = PositiveNegativeChart(rep: rep) + } + miniChart.data = data.qMiniChart + miniChart.maxValue = miniChart.data?.qMax ?? Double.infinity + miniChart.minValue = miniChart.data?.qMin ?? -Double.infinity + miniChart.globalMaxValue = rep.globalMax ?? Double.infinity + miniChart.globalMinValue = rep.globalMin ?? -Double.infinity + miniChart.yScale = miniChart.maxValue + if(rep.miniChart?.yAxis?.scale == "global") { + miniChart.yScale = miniChart.globalMaxValue + } + } + + override func draw(_ rect: CGRect) { + guard let ctx = UIGraphicsGetCurrentContext() else { return } + miniChart.render(ctx, rect: rect) + } +} diff --git a/ios/MiniDotGraph.swift b/ios/MiniDotGraph.swift new file mode 100644 index 00000000..9ec77af3 --- /dev/null +++ b/ios/MiniDotGraph.swift @@ -0,0 +1,45 @@ +// +// MiniDotGraph.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-03. +// + +import Foundation +// +// MiniSparkLineChart.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-03. +// + +import Foundation +class MiniDotGraph : MiniSparkLineChart { + + override func render(_ ctx: CGContext, rect: CGRect) { + guard let data = data else {return} + guard let rows = data.qMatrix else {return} + clearDots() + if (rect.size.height == 0) {return} + ctx.clear(rect) + getBandWidth(rect: rect, data: data) + getScale(rect:rect, data:data) + + var x = padding + horizontalPadding / 2; + var index = 1 + let halfLine = rect.height / 2 + startPath(rows, ctx, x, rect) + x += padding * 2 + bandWidth + for row in rows.dropFirst() { + let value = row[1].qNum ?? 1.0 + let height = value * scale; + let y = rect.height - height + let x2 = x + padding * 2 + bandWidth + drawDot(index, value: value, count: rows.count, x: x2, y: y + getVerticalPadding(y, halfLine: halfLine)) + x += padding * 2 + bandWidth + index += 1 + } + drawDots(ctx) + } + +} diff --git a/ios/MiniSparkLineChart.swift b/ios/MiniSparkLineChart.swift new file mode 100644 index 00000000..29e027da --- /dev/null +++ b/ios/MiniSparkLineChart.swift @@ -0,0 +1,141 @@ +// +// MiniSparkLineChart.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-03. +// + +import Foundation +class MiniSparkLineChart : MiniChartRenderer { + + var maxDots = UIBezierPath() + var firstDot = UIBezierPath() + var lastDot = UIBezierPath() + var minDots = UIBezierPath() + var mainDots = UIBezierPath() + var linePath = UIBezierPath() + + override func render(_ ctx: CGContext, rect: CGRect) { + guard let data = data else {return} + guard let rows = data.qMatrix else {return} + linePath = UIBezierPath() + clearDots() + if (rect.size.height == 0) {return} + ctx.clear(rect) + getBandWidth(rect: rect, data: data) + getScale(rect:rect, data:data) + + var x = padding + horizontalPadding / 2; + var index = 1 + startPath(rows, ctx, x, rect) + x += padding * 2 + bandWidth + let halfLine = rect.height / 2 + for row in rows.dropFirst() { + let value = row[1].qNum ?? 1.0 + let height = value * scale; + let y = rect.height - height; + let x2 = x + padding * 2 + bandWidth + let vpadding = getVerticalPadding(y, halfLine: halfLine) + linePath.addLine(to: CGPoint(x: x2, y: y + vpadding)) + drawDot(index, value: value, count: rows.count, x: x2, y: y + vpadding) + x += padding * 2 + bandWidth + index += 1 + } + if(mainColor != .clear) { + mainColor.set() + ctx.addPath(linePath.cgPath); + ctx.strokePath() + } + + drawDots(ctx) + } + + func getVerticalPadding(_ y: Double, halfLine: Double) -> Double { + let vp = verticalPadding / 2; + return y > halfLine ? -vp : vp + } + + func clearDots() { + + maxDots = UIBezierPath() + firstDot = UIBezierPath() + lastDot = UIBezierPath() + minDots = UIBezierPath() + mainDots = UIBezierPath() + } + + func drawDot(_ index: Int, value: Double, count: Int, x: CGFloat, y: CGFloat) { + if(index == 0 && firstColor != .clear) { + addArc(firstDot, x: x, y: y) + return; + } + + if(index == count - 1 && lastColor != .clear) { + addArc(lastDot, x: x, y: y) + return; + } + + if(value == maxValue && maxColor != .clear) { + addArc(maxDots, x: x, y: y) + return; + } + + if(value == minValue && minColor != .clear) { + addArc(minDots, x: x, y: y) + return; + } + + if(showDots) { + addArc(mainDots, x: x, y: y) + } + + } + + func addArc(_ path: UIBezierPath, x: CGFloat, y: CGFloat) { + path.move(to: CGPoint(x: x, y: y)) + path.addArc(withCenter: CGPoint(x: x, y: y), radius: 2, startAngle: 0, endAngle: CGFloat(Double.pi * 2), clockwise: true) + path.close() + } + + func startPath(_ rows: [[MatrixCell]], _ ctx: CGContext, _ x: Double, _ rect: CGRect) { + let startValue = rows[0][1].qNum ?? 0 + let height = startValue * scale; + let y = rect.height - height; + let vpadding = getVerticalPadding(y, halfLine: rect.height / 2) + linePath.move(to: CGPoint(x: x, y: y + vpadding)) + drawDot(0, value: startValue, count: rows.count, x: x, y: y + vpadding) + } + + func drawDots(_ ctx: CGContext) { + if(maxColor != .clear) { + maxColor.set() + ctx.addPath(maxDots.cgPath) + ctx.fillPath() + + } + + if(minColor != .clear) { + minColor.set() + ctx.addPath(minDots.cgPath) + ctx.fillPath() + } + + if(firstColor != .clear) { + firstColor.set() + ctx.addPath(firstDot.cgPath) + ctx.fillPath() + } + + if(lastColor != .clear) { + lastColor.set() + ctx.addPath(lastDot.cgPath) + ctx.fillPath() + } + + if(showDots) { + mainColor.set() + ctx.addPath(mainDots.cgPath) + ctx.fillPath() + } + } +} diff --git a/ios/PositiveNegativeChart.swift b/ios/PositiveNegativeChart.swift new file mode 100644 index 00000000..5547d7fd --- /dev/null +++ b/ios/PositiveNegativeChart.swift @@ -0,0 +1,44 @@ +// +// PositiveNegativeChart.swift +// react-native-simple-grid +// +// Created by Vittorio Cellucci on 2022-08-04. +// + +import Foundation +class PositiveNegativeChart : MiniSparkLineChart { + + override func render(_ ctx: CGContext, rect: CGRect) { + guard let data = data else {return} + guard let rows = data.qMatrix else {return} + if (rect.size.height == 0) {return} + ctx.clear(rect) + getBandWidth(rect: rect, data: data) + getScale(rect:rect, data:data) + var x = padding + horizontalPadding / 2; + var index = 0 + let barHeight = (rect.height - 8) / 2; + for row in rows { + let value = row[1].qNum ?? 1.0 + let height = barHeight + var y = rect.height - height - 4 + if (value > 0) { + y -= barHeight + } + let rect = CGRect(x: x, y: y, width: bandWidth, height:height) + setColor(index, value: value, count: rows.count) + ctx.fill(rect) + x += padding * 2 + bandWidth + index += 1 + } + } + + override func setColor(_ index: Int, value: Double, count: Int) { + if (value < 0) { + negativeColor.set() + } else { + positiveColor.set() + } + } + +}