Skip to content

Commit

Permalink
feat: add a new chart type - column
Browse files Browse the repository at this point in the history
  • Loading branch information
shengxu7 committed Jun 6, 2020
1 parent d25cc9a commit 06bb4a6
Show file tree
Hide file tree
Showing 7 changed files with 395 additions and 26 deletions.
89 changes: 89 additions & 0 deletions Sources/FioriCharts/Charts/ColumnChart/ColumnChart.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
//
// ColumnChart.swift
// FioriCharts
//
// Created by Xu, Sheng on 6/3/20.
//

import SwiftUI

let ColumnGapFraction: CGFloat = 0.333333

struct ColumnChart: View {
@ObservedObject var model: ChartModel

public init(_ chartModel: ChartModel) {
self.model = chartModel
}

var body: some View {
XYAxisChart(model,
axisDataSource: ColumnAxisDataSource(),
chartView: ColumnView(model),
indicatorView: ColumnIndicatorView(model))
}
}

class ColumnAxisDataSource: DefaultAxisDataSource {
override func xAxisLabels(_ model: ChartModel, rect: CGRect) -> [AxisTitle] {
return xAxisGridLineLabels(model, rect: rect, isLabel: true)
}

override func xAxisGridlines(_ model: ChartModel, rect: CGRect) -> [AxisTitle] {
return xAxisGridLineLabels(model, rect: rect, isLabel: false)
}

func xAxisGridLineLabels(_ model: ChartModel, rect: CGRect, isLabel: Bool) -> [AxisTitle] {
var ret: [AxisTitle] = []
let maxDataCount = model.numOfCategories(in: 0)
let startIndex = 0
let endIndex = maxDataCount - 1

if endIndex < 0 {
return ret
}

let columnXIncrement = 1.0 / (CGFloat(maxDataCount) - ColumnGapFraction / (1.0 + ColumnGapFraction))
let clusterWidth = columnXIncrement / (1.0 + ColumnGapFraction)
let labelsIndex = model.categoryAxis.labelLayoutStyle == .allOrNothing ? Array(startIndex ... endIndex) : (startIndex != endIndex ? [startIndex, endIndex] : [startIndex])

for (index, i) in labelsIndex.enumerated() {
var x = rect.origin.x + (columnXIncrement * CGFloat(i) + clusterWidth / 2.0) * rect.size.width

let title = ChartUtility.categoryValue(model, categoryIndex: i) ?? ""
if model.categoryAxis.labelLayoutStyle == .range && isLabel {
let size = title.boundingBoxSize(with: model.categoryAxis.labels.fontSize)
if index == 0 {
x = rect.origin.x + min(size.width, (rect.size.width - 2) / 2) / 2
} else {
x = rect.origin.x + (columnXIncrement * CGFloat(i) + clusterWidth) * rect.size.width - min(size.width, (rect.size.width - 2) / 2) / 2
}
}

ret.append(AxisTitle(index: i,
title: title,
pos: CGPoint(x: x, y: 0)))
}

return ret
}
}

// swiftlint:disable force_cast
struct ColumnChart_Previews: PreviewProvider {
static var previews: some View {
let models: [ChartModel] = Tests.lineModels.map {
let model = $0.copy() as! ChartModel
model.chartType = .column
return model
}

return Group {
ForEach(models) {
ColumnChart($0)
.frame(width: 330, height: 220, alignment: .topLeading)
.previewLayout(.sizeThatFits)
}
}
}
}
41 changes: 41 additions & 0 deletions Sources/FioriCharts/Charts/ColumnChart/ColumnIndicatorView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// ColumnIndicatorView.swift
// FioriCharts
//
// Created by Xu, Sheng on 6/3/20.
//

import SwiftUI

struct ColumnIndicatorView: View {
@ObservedObject var model: ChartModel
@Environment(\.colorScheme) var colorScheme
@Environment(\.layoutDirection) var layoutDirection

init(_ model: ChartModel) {
self.model = model
}

var body: some View {
Text(" ")
}
}

// swiftlint:disable force_cast
struct ColumnSelectionView_Previews: PreviewProvider {
static var previews: some View {
let models: [ChartModel] = Tests.lineModels.map {
let model = $0.copy() as! ChartModel
model.chartType = .column
return model
}

return Group {
ForEach(models) {
ColumnIndicatorView($0)
.frame(width: 330, height: 220, alignment: .topLeading)
.previewLayout(.sizeThatFits)
}
}
}
}
186 changes: 186 additions & 0 deletions Sources/FioriCharts/Charts/ColumnChart/ColumnView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
//
// ColumnView.swift
// FioriCharts
//
// Created by Xu, Sheng on 6/3/20.
//

import SwiftUI

struct ColumnView: View {
@ObservedObject var model: ChartModel
@Environment(\.colorScheme) var colorScheme

init(_ model: ChartModel) {
self.model = model
}

var body: some View {
GeometryReader { proxy in
self.makeBody(in: proxy.frame(in: .local))
}
}

func makeBody(in rect: CGRect) -> some View {
let tickValues = model.numericAxisTickValues
let maxDataCount = model.numOfCategories(in: 0)
let columnXIncrement = 1.0 / (CGFloat(maxDataCount) - ColumnGapFraction / (1.0 + ColumnGapFraction))
let clusterWidth = columnXIncrement / (1.0 + ColumnGapFraction)
let clusterSpace: CGFloat = rect.size.width * (1.0 - clusterWidth * CGFloat(maxDataCount)) / CGFloat(max((maxDataCount - 1), 1))

let plotData = generateClusteredPlotData()

return VStack(alignment: .center, spacing: 0) {
if plotData.isEmpty {
NoDataView()
} else {
// positive value columns and their value column lables
HStack(alignment: .bottom, spacing: clusterSpace) {
// positive values
if tickValues.plotMinimum >= 0 {
ForEach(plotData, id: \.self) { series in
HStack(alignment: .bottom, spacing: 0) {
ForEach(series, id: \.self) { item in
Rectangle()
.fill(self.model.seriesAttributes[item.seriesIndex].palette.fillColor.color(self.colorScheme))
.frame(width: item.rect.size.width * rect.size.width, height: item.rect.size.height * rect.size.height)
}
}
}
} else if tickValues.plotMaximum <= 0 { // negative values
ForEach(plotData, id: \.self) { series in
HStack(alignment: .bottom, spacing: 0) {
ForEach(series, id: \.self) { item in
VStack(alignment: .center, spacing: 0) {
Rectangle()
.fill(self.model.seriesAttributes[item.seriesIndex].palette.fillColor.color(self.colorScheme))
.frame(width: item.rect.size.width * rect.size.width, height: item.rect.size.height * rect.size.height)
Spacer(minLength: 0)
}
}
}
}
} else { // mixed values
ForEach(plotData, id: \.self) { series in
HStack(alignment: .bottom, spacing: 0) {
ForEach(series, id: \.self) { item in
VStack(spacing: 0) {
// positive area
VStack(spacing: 0) {
Spacer(minLength: 0)
Rectangle()
.fill(self.model.seriesAttributes[item.seriesIndex].palette.fillColor.color(self.colorScheme))
.frame(width: item.rect.size.width * rect.size.width, height: self.columnHeight(from: item, isPositiveArea: true) * rect.size.height)
}.frame(height: (1 - tickValues.plotBaselinePosition) * rect.size.height)

// negative area
VStack(spacing: 0) {
Rectangle()
.fill(self.model.seriesAttributes[item.seriesIndex].palette.fillColor.color(self.colorScheme))
.frame(width: item.rect.size.width * rect.size.width,
height: self.columnHeight(from: item, isPositiveArea: false) * rect.size.height)
Spacer(minLength: 0)
}.frame(height: tickValues.plotBaselinePosition * rect.size.height)
}
}
}
}
}
}
}
}
}

func columnHeight(from rectData: ChartPlotRectData, isPositiveArea: Bool) -> CGFloat {
if (rectData.value >= 0 && isPositiveArea) || (rectData.value < 0 && !isPositiveArea) {
return rectData.rect.size.height
} else {
return 0
}
}

func generateClusteredPlotData() -> [[ChartPlotRectData]] {
var result: [[ChartPlotRectData]] = []
let seriesCount = model.numOfSeries()
let maxDataCount = model.numOfCategories(in: 0)

let columnXIncrement = 1.0 / (CGFloat(maxDataCount) - ColumnGapFraction / (1.0 + ColumnGapFraction))
let clusterWidth = columnXIncrement / (1.0 + ColumnGapFraction)
let columnWidth = clusterWidth / CGFloat(seriesCount)

let tickValues = model.numericAxisTickValues
// let minValue = tickValues.plotMinimum
// let maxValue = tickValues.plotMaximum
let yScale = tickValues.plotScale
let baselinePosition = tickValues.plotBaselinePosition
let baselineValue = tickValues.plotBaselineValue
let corruptDataHeight = yScale / 1000

var clusteredX: CGFloat
//
// Loop through data points
//
for categoryIndex in 0 ..< maxDataCount {
var seriesResult: [ChartPlotRectData] = []

//
// Loop through series
//
for seriesIndex in 0 ..< seriesCount {
//
// Calculate and define clustered column x positions.
//
clusteredX = columnXIncrement * CGFloat(categoryIndex)
clusteredX += columnWidth * CGFloat(seriesIndex)
var columnHeight = corruptDataHeight
var clusteredY = baselinePosition
var rawValue: CGFloat = 0
//
// Fetch value
//
let value = model.plotItemValue(at: seriesIndex, category: categoryIndex, dimension: 0)

if let val = value {
rawValue = CGFloat(val)
if val >= 0.0 {
columnHeight = yScale * (CGFloat(val) - baselineValue)
} else {
columnHeight = -yScale * (CGFloat(val) - baselineValue)
clusteredY = baselinePosition - columnHeight
}
}

seriesResult.append(ChartPlotRectData(seriesIndex: seriesIndex,
categoryIndex: categoryIndex,
value: rawValue,
x: clusteredX,
y: clusteredY,
width: columnWidth,
height: columnHeight))
}

result.append(seriesResult)
}

return result
}
}

// swiftlint:disable force_cast
struct ColumnView_Previews: PreviewProvider {
static var previews: some View {
let models: [ChartModel] = Tests.lineModels.map {
let model = $0.copy() as! ChartModel
model.chartType = .column
return model
}

return Group {
ForEach(models) {
ColumnView($0)
.frame(width: 330, height: 220, alignment: .topLeading)
.previewLayout(.sizeThatFits)
}
}
}
}
Loading

0 comments on commit 06bb4a6

Please sign in to comment.