-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Reed Es
committed
Nov 19, 2021
0 parents
commit c8294c4
Showing
5 changed files
with
437 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
.DS_Store | ||
/.build | ||
/Packages | ||
/*.xcodeproj | ||
xcuserdata/ | ||
DerivedData/ | ||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// swift-tools-version:5.3 | ||
|
||
import PackageDescription | ||
|
||
let package = Package( | ||
name: "NiceScale", | ||
products: [ | ||
.library(name: "NiceScale", targets: ["NiceScale"]), | ||
], | ||
dependencies: [ | ||
.package(url: "https://github.com/apple/swift-numerics", from: "1.0.0"), | ||
], | ||
targets: [ | ||
.target( | ||
name: "NiceScale", | ||
dependencies: [ | ||
.product(name: "Numerics", package: "swift-numerics"), | ||
], | ||
path: "Sources" | ||
), | ||
.testTarget( | ||
name: "NiceScaleTests", | ||
dependencies: ["NiceScale"], | ||
path: "Tests" | ||
), | ||
] | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
# SwiftNiceScale | ||
|
||
Generate 'nice' numbers for label ticks over a range, such as for y-axis on a chart. | ||
|
||
Available as an open source Swift library to be incorporated in other apps. | ||
|
||
_SwiftNiceScale_ is part of the [OpenAlloc](https://github.com/openalloc) family of open source Swift software tools. | ||
|
||
## NiceScale | ||
|
||
Adapted from pseudo-code in *Graphics Gems, Volume 1* by Andrew S. Glassner (1995). Using the example from the book: | ||
|
||
```swift | ||
let ns = NiceScale(105...543, desiredTicks: 5) | ||
|
||
print("nice range=\(ns.range)") | ||
=> "nice range=100.0...600.0" | ||
|
||
print("tick interval=\(ns.tickInterval)") | ||
=> "tick interval=100.0" | ||
|
||
print("labels=\(ns.tickValues)") | ||
=> "labels=[100.0, 200.0, 300.0, 400.0, 500.0, 600.0]" | ||
``` | ||
|
||
## Types | ||
|
||
The `ValueRange` type is declared within `NiceScale`, where `T` is your `BinaryFloatingPoint` data type: | ||
|
||
```swift | ||
typealias ValueRange = ClosedRange<T> | ||
``` | ||
|
||
## Instance Properties and Methods | ||
|
||
#### Initializer | ||
|
||
- `init(_ rawRange: NiceScale<T>.ValueRange, desiredTicks: Int)` - create a new `NiceScale` instance | ||
|
||
The initialization values are also available as properties: | ||
|
||
- `let rawRange: NiceScale<T>.ValueRange` - the source range from which to calculate a nice scaling | ||
|
||
- `let desiredTicks: Int` - the desired number of ticks in the scaling (default: 10) | ||
|
||
#### Instance Properties | ||
|
||
Properties are lazy, meaning that they are only calculated when first needed. | ||
|
||
- `var extent: T` - The distance between bounds of the range. | ||
|
||
- `var hasNegativeRange: Bool` - If true, the range includes negative values. | ||
|
||
- `var hasPositiveRange: Bool` - If true, the range includes positive values. | ||
|
||
- `var negativeExtent: T` - If there’s a negative portion of range, the distance between its lower bound and 0. A non-negative value. | ||
|
||
- `var negativeExtentUnit: T?` - The negativeExtent, expressed as unit value in the range 0…1. | ||
|
||
- `var negativeRange: NiceScale<T>.ValueRange` - The portion of the range that is negative. 0…0 if none. | ||
|
||
- `var positiveExtent: T` - If there’s a positive portion of range, the distance between its upper bound and 0. A non-negative value. | ||
|
||
- `var positiveExtentUnit: T?` - The positiveExtent, expressed as unit value in the range 0…1. | ||
|
||
- `var positiveRange: NiceScale<T>.ValueRange` - The portion of the range that is positive. 0…0 if none. | ||
|
||
- `var range: NiceScale<T>.ValueRange` - The calculated ‘nice’ range, which should include the ‘raw’ range used to initialize this object. | ||
|
||
- `var tickFractionDigits: Int` - Number of fractional digits to show in tick label values. | ||
|
||
- `var tickInterval: T` - The distance between ticks in the range. | ||
|
||
- `var tickValues: [T]` - The values for the ticks in the range. | ||
|
||
- `var ticks: Int` - The number of ticks in the range. This may differ from the desiredTicks used to initialize the object. | ||
|
||
#### Instance Methods | ||
|
||
- `func scaleToUnit(T) -> T` - Scale value to 0…1 in the range. | ||
|
||
- `func scaleToUnitNegative(T) -> T` - Scale value to 0…1 in the negative portion of range, if any. | ||
|
||
- `func scaleToUnitPositive(T) -> T` - Scale value to 0…1 in the positive portion of range, if any. | ||
|
||
## See Also | ||
|
||
Swift open-source libraries (by the same author): | ||
|
||
* [AllocData](https://github.com/openalloc/AllocData) - standardized data formats for investing-focused apps and tools | ||
* [FINporter](https://github.com/openalloc/FINporter) - library and command-line tool to transform various specialized finance-related formats to the standardized schema of AllocData | ||
* [SwiftCompactor](https://github.com/openalloc/SwiftCompactor) - formatters for the concise display of Numbers, Currency, and Time Intervals | ||
* [SwiftModifiedDietz](https://github.com/openalloc/SwiftModifiedDietz) - A tool for calculating portfolio performance using the Modified Dietz method | ||
* [SwiftRegressor](https://github.com/openalloc/SwiftRegressor) - a linear regression tool that’s flexible and easy to use | ||
* [SwiftSimpleTree](https://github.com/openalloc/SwiftSimpleTree) - a nested data structure that’s flexible and easy to use | ||
|
||
And commercial apps using this library (by the same author): | ||
|
||
* [FlowAllocator](https://flowallocator.app/FlowAllocator/index.html) - portfolio rebalancing tool for macOS | ||
* [FlowWorth](https://flowallocator.app/FlowWorth/index.html) - a new portfolio performance and valuation tracking tool for macOS | ||
|
||
## License | ||
|
||
Copyright 2021 FlowAllocator LLC | ||
|
||
Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at | ||
|
||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0) | ||
|
||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
|
||
## Contributing | ||
|
||
The contributions of other regressors, such as a polynominal regressor, would be most welcome! | ||
|
||
Other contributions are welcome too. You are encouraged to submit pull requests to fix bugs, improve documentation, or offer new features. | ||
|
||
The pull request need not be a production-ready feature or fix. It can be a draft of proposed changes, or simply a test to show that expected behavior is buggy. Discussion on the pull request can proceed from there. | ||
|
||
Contributions should ultimately have adequate test coverage. See tests for current entities to see what coverage is expected. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,177 @@ | ||
// | ||
// NiceScale.swift | ||
// | ||
// Copyright 2021 FlowAllocator LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
// | ||
|
||
import Foundation | ||
import Numerics | ||
|
||
public final class NiceScale<T: BinaryFloatingPoint & Real> { | ||
|
||
public typealias ValueRange = ClosedRange<T> | ||
|
||
public let rawRange: ValueRange | ||
public let desiredTicks: Int | ||
|
||
public init(_ rawRange: ValueRange, desiredTicks: Int = 10) { | ||
self.rawRange = rawRange | ||
self.desiredTicks = desiredTicks | ||
} | ||
|
||
// MARK: - Range | ||
|
||
/// The calculated 'nice' range, which should include the 'raw' range used to initialize this object. | ||
public lazy var range: ValueRange = { | ||
guard tickInterval != 0 else { return 0...0 } // avoid NaN error in creating range | ||
let min = floor(rawRange.lowerBound / tickInterval) * tickInterval | ||
let max = ceil(rawRange.upperBound / tickInterval) * tickInterval | ||
return min ... max | ||
}() | ||
|
||
/// The distance between bounds of the range. | ||
public lazy var extent: T = { | ||
range.upperBound - range.lowerBound | ||
}() | ||
|
||
// MARK: - Ticks | ||
|
||
/// The number of ticks in the range. This may differ from the desiredTicks used to initialize the object. | ||
public lazy var ticks: Int = { | ||
guard tickInterval > 0 else { return 0 } | ||
return Int(extent / tickInterval) + 1 | ||
}() | ||
|
||
/// The values for the ticks in the range. | ||
public lazy var tickValues: [T] = { | ||
(0..<ticks).map { | ||
range.lowerBound + (T($0) * tickInterval) | ||
} | ||
}() | ||
|
||
/// The distance between ticks in the range. | ||
public lazy var tickInterval: T = { | ||
let rawExtent = rawRange.upperBound - rawRange.lowerBound | ||
let niceExtent = niceify(rawExtent, round: false) | ||
return niceify(niceExtent / T(desiredTicks - 1), round: true) | ||
}() | ||
|
||
/// Number of fractional digits to show in tick label values. | ||
public lazy var tickFractionDigits: Int = { | ||
let exponent = floor(T.log10(tickInterval)) | ||
let nfrac = max(-1 * exponent, 0.0) | ||
return Int(nfrac) | ||
}() | ||
|
||
// MARK: - Positive/Negative Range | ||
|
||
/// If true, the range includes positive values. | ||
public lazy var hasPositiveRange: Bool = { | ||
range.upperBound > 0 | ||
}() | ||
|
||
/// If true, the range includes negative values. | ||
public lazy var hasNegativeRange: Bool = { | ||
range.lowerBound < 0 | ||
}() | ||
|
||
/// The portion of the range that is positive. 0...0 if none. | ||
public lazy var positiveRange: ValueRange = { | ||
max(0, range.lowerBound)...max(0, range.upperBound) | ||
}() | ||
|
||
/// The portion of the range that is negative. 0...0 if none. | ||
public lazy var negativeRange: ValueRange = { | ||
min(0, range.lowerBound)...min(0, range.upperBound) | ||
}() | ||
|
||
/// If there's a positive portion of range, the distance between its upper bound and 0. A non-negative value. | ||
public lazy var positiveExtent: T = { | ||
positiveRange.upperBound - positiveRange.lowerBound | ||
}() | ||
|
||
/// If there's a negative portion of range, the distance between its lower bound and 0. A non-negative value. | ||
public lazy var negativeExtent: T = { | ||
negativeRange.upperBound - negativeRange.lowerBound | ||
}() | ||
|
||
/// The positiveExtent, expressed as unit value in the range 0...1. | ||
public lazy var positiveExtentUnit: T? = { | ||
guard extent != T.zero else { return nil } | ||
return positiveExtent / extent | ||
}() | ||
|
||
/// The negativeExtent, expressed as unit value in the range 0...1. | ||
public lazy var negativeExtentUnit: T? = { | ||
guard extent != T.zero else { return nil } | ||
return negativeExtent / extent | ||
}() | ||
} | ||
|
||
extension NiceScale { | ||
|
||
/// Returns a "nice" number approximately equal to range. | ||
/// If round = true, rounds the number, otherwise returns its ceiling. | ||
private func niceify(_ x: T, round: Bool) -> T { | ||
let exp = floor(T.log10(x)) // exponent of x | ||
let f = x / T.pow(10, exp) // fractional part of x, in 1...10 | ||
let niceFraction: T = { | ||
if (round) { | ||
if (f < 1.5) { | ||
return 1 | ||
} else if (f < 3) { | ||
return 2 | ||
} else if (f < 7) { | ||
return 5 | ||
} else { | ||
return 10 | ||
} | ||
} else { | ||
if (f <= 1) { | ||
return 1 | ||
} else if (f <= 2) { | ||
return 2 | ||
} else if (f <= 5) { | ||
return 5 | ||
} else { | ||
return 10 | ||
} | ||
} | ||
}() | ||
return niceFraction * T.pow(10, exp) | ||
} | ||
} | ||
|
||
/// MARK: - Scaling methods | ||
|
||
extension NiceScale { | ||
/// Scale value to 0...1 in the range. | ||
@inlinable | ||
public func scaleToUnit(_ val: T) -> T { | ||
(val - range.lowerBound) / extent | ||
} | ||
|
||
/// Scale value to 0...1 in the positive portion of range, if any. | ||
@inlinable | ||
public func scaleToUnitPositive(_ val: T) -> T { | ||
(val - positiveRange.lowerBound) / positiveExtent | ||
} | ||
|
||
/// Scale value to 0...1 in the negative portion of range, if any. | ||
@inlinable | ||
public func scaleToUnitNegative(_ val: T) -> T { | ||
1 - ((val - negativeRange.lowerBound) / negativeExtent) | ||
} | ||
} |
Oops, something went wrong.