Skip to content

Commit 74ee01f

Browse files
committed
reverse enabled and tested on base level scales
1 parent 271c383 commit 74ee01f

14 files changed

+365
-29
lines changed

Sources/SwiftVizScale/BandScale.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,14 +180,21 @@ public struct BandScale<CategoryType: Comparable, OutputType: ConvertibleWithDou
180180
/// - Parameter value: A discrete item from the list provided as the domain for the scale.
181181
/// - Returns: A band that wraps the category found in the domain with start and end values for the range of the band, or `nil` if the value isn't contained by the domain.
182182
public func scale(_ value: CategoryType) -> Band<CategoryType, OutputType>? {
183-
guard let index = domain.firstIndex(of: value), let step = step(), let width = width() else {
183+
guard let step = step(), let width = width() else {
184184
return nil
185185
}
186-
if width <= 0 {
186+
if width <= 0 || !domain.contains(value) {
187187
// when there's more padding than available space for the categories
188+
// OR
189+
// the value to be scaled isn't within the domain
188190
return nil
189191
}
190-
let doublePosition = Double(index) // 1
192+
let doublePosition: Double
193+
if reversed {
194+
doublePosition = Double(domain.reversed().firstIndex(of: value)!)
195+
} else {
196+
doublePosition = Double(domain.firstIndex(of: value)!)
197+
}
191198
let startLocation = paddingOuter.toDouble() + (doublePosition * step)
192199
let stopLocation = startLocation + width
193200
if round {
@@ -239,8 +246,13 @@ public struct BandScale<CategoryType: Comparable, OutputType: ConvertibleWithDou
239246
}
240247
// calculate the closest index
241248
let rangeExtentWithoutOuterPadding = upperRange.toDouble() - lowerRange.toDouble() - 2 * paddingOuter.toDouble()
242-
let unitRangeValue = (location.toDouble() - paddingOuter.toDouble()) / rangeExtentWithoutOuterPadding
243-
let rangeValueExpandedToCountDomain = unitRangeValue * Double(domain.count - 1)
249+
let indexedRangeValue: Double
250+
if reversed {
251+
indexedRangeValue = (upperRange.toDouble() - paddingOuter.toDouble() - location.toDouble()) / rangeExtentWithoutOuterPadding
252+
} else {
253+
indexedRangeValue = (location.toDouble() - paddingOuter.toDouble()) / rangeExtentWithoutOuterPadding
254+
}
255+
let rangeValueExpandedToCountDomain = indexedRangeValue * Double(domain.count - 1)
244256

245257
let closestIndex = Int(rangeValueExpandedToCountDomain.rounded())
246258
if let band = scale(domain[closestIndex]) {

Sources/SwiftVizScale/ContinuousScale.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -560,6 +560,5 @@ func normalize<T: Real>(_ x: T, lower: T, higher: T) -> T {
560560
/// interpolate(a, b)(t) takes a parameter t in [0,1] and
561561
/// returns the corresponding range value x in [a,b].
562562
func interpolate<T: Real>(_ x: T, lower: T, higher: T) -> T {
563-
precondition(lower < higher)
564-
return lower * (1 - x) + higher * x
563+
lower * (1 - x) + higher * x
565564
}

Sources/SwiftVizScale/LinearScale.swift

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -211,13 +211,13 @@ public struct LinearScale<InputType: ConvertibleWithDouble & NiceValue, OutputTy
211211
return nil
212212
}
213213
let normalizedInput = normalize(domainValue.toDouble(), lower: domainLower.toDouble(), higher: domainHigher.toDouble())
214+
let result: Double
214215
if reversed {
215-
let result: Double = interpolate(normalizedInput, lower: rangeHigher.toDouble(), higher: rangeLower.toDouble())
216-
return OutputType.fromDouble(result)
216+
result = interpolate(normalizedInput, lower: rangeHigher.toDouble(), higher: rangeLower.toDouble())
217217
} else {
218-
let result: Double = interpolate(normalizedInput, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
219-
return OutputType.fromDouble(result)
218+
result = interpolate(normalizedInput, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
220219
}
220+
return OutputType.fromDouble(result)
221221
}
222222

223223
/// Transforms the input value using a linear function to the resulting value into the range you provide.
@@ -255,15 +255,14 @@ public struct LinearScale<InputType: ConvertibleWithDouble & NiceValue, OutputTy
255255
}
256256
// inverts the scale, taking a value in the output range and returning the relevant value from the input domain
257257
let normalizedRangeValue = normalize(rangeValue.toDouble(), lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
258+
let mappedToDomain: Double
258259
if reversed {
259-
let mappedToDomain = interpolate(normalizedRangeValue, lower: domainHigher.toDouble(), higher: domainLower.toDouble())
260-
let castToInputType = InputType.fromDouble(mappedToDomain)
261-
return transformAgainstDomain(castToInputType)
260+
mappedToDomain = interpolate(normalizedRangeValue, lower: domainHigher.toDouble(), higher: domainLower.toDouble())
262261
} else {
263-
let mappedToDomain = interpolate(normalizedRangeValue, lower: domainLower.toDouble(), higher: domainHigher.toDouble())
264-
let castToInputType = InputType.fromDouble(mappedToDomain)
265-
return transformAgainstDomain(castToInputType)
262+
mappedToDomain = interpolate(normalizedRangeValue, lower: domainLower.toDouble(), higher: domainHigher.toDouble())
266263
}
264+
let castToInputType = InputType.fromDouble(mappedToDomain)
265+
return transformAgainstDomain(castToInputType)
267266
}
268267

269268
/// Transforms a value within the range into the associated domain value.

Sources/SwiftVizScale/LogScale.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,12 @@ public struct LogScale<InputType: ConvertibleWithDouble & NiceValue, OutputType:
197197
let logDomainLower = log10(domainLower.toDouble())
198198
let logDomainHigher = log10(domainHigher.toDouble())
199199
let normalizedValueOnLogDomain = normalize(logDomainValue, lower: logDomainLower, higher: logDomainHigher)
200-
let valueMappedToRange = interpolate(normalizedValueOnLogDomain, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
200+
let valueMappedToRange: Double
201+
if reversed {
202+
valueMappedToRange = interpolate(normalizedValueOnLogDomain, lower: rangeHigher.toDouble(), higher: rangeLower.toDouble())
203+
} else {
204+
valueMappedToRange = interpolate(normalizedValueOnLogDomain, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
205+
}
201206
return OutputType.fromDouble(valueMappedToRange)
202207
}
203208

@@ -237,7 +242,12 @@ public struct LogScale<InputType: ConvertibleWithDouble & NiceValue, OutputType:
237242
let normalizedRangeValue = normalize(rangeValue.toDouble(), lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
238243
let logDomainLower = log10(domainLower.toDouble())
239244
let logDomainHigher = log10(domainHigher.toDouble())
240-
let linearInterpolatedValue = interpolate(Double(normalizedRangeValue), lower: logDomainLower, higher: logDomainHigher)
245+
let linearInterpolatedValue: Double
246+
if reversed {
247+
linearInterpolatedValue = interpolate(Double(normalizedRangeValue), lower: logDomainHigher, higher: logDomainLower)
248+
} else {
249+
linearInterpolatedValue = interpolate(Double(normalizedRangeValue), lower: logDomainLower, higher: logDomainHigher)
250+
}
241251
let domainValue = pow(10, linearInterpolatedValue)
242252
let downcastDomainValue = InputType.fromDouble(domainValue)
243253
return transformAgainstDomain(downcastDomainValue)

Sources/SwiftVizScale/PointScale.swift

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -165,15 +165,20 @@ public struct PointScale<CategoryType: Comparable, OutputType: ConvertibleWithDo
165165
/// - Parameter value: A discrete item from the list provided as the domain for the scale.
166166
/// - Returns: A location along the range that indicates a point that matches with the value you provided, or `nil` if the value isn't contained by the domain.
167167
public func scale(_ value: CategoryType) -> OutputType? {
168-
guard let index = domain.firstIndex(of: value), let step = step() else {
168+
guard let step = step() else {
169169
return nil
170170
}
171-
if step <= 0 {
171+
if step <= 0 || !domain.contains(value) {
172172
// when there's more padding than available space for the categories
173173
return nil
174174
}
175-
let doublePosition = Double(index) // 1
176-
let location = padding.toDouble() + (doublePosition * step)
175+
let doublePosition: Double
176+
if reversed {
177+
doublePosition = Double(domain.reversed().firstIndex(of: value)!)
178+
} else {
179+
doublePosition = Double(domain.firstIndex(of: value)!)
180+
}
181+
let location = padding.toDouble() + (doublePosition * step) + (step / 2)
177182
if round {
178183
return OutputType.fromDouble(location.rounded())
179184
}
@@ -223,8 +228,13 @@ public struct PointScale<CategoryType: Comparable, OutputType: ConvertibleWithDo
223228
}
224229
// calculate the closest index
225230
let rangeExtentWithoutPadding = upperRange.toDouble() - lowerRange.toDouble() - 2 * padding.toDouble()
226-
let unitRangeValue = (location.toDouble() - padding.toDouble()) / rangeExtentWithoutPadding
227-
let rangeValueExpandedToCountDomain = unitRangeValue * Double(domain.count - 1)
231+
let indexedRangeValue: Double
232+
if reversed {
233+
indexedRangeValue = (upperRange.toDouble() - padding.toDouble() - location.toDouble()) / rangeExtentWithoutPadding
234+
} else {
235+
indexedRangeValue = (location.toDouble() - padding.toDouble()) / rangeExtentWithoutPadding
236+
}
237+
let rangeValueExpandedToCountDomain = indexedRangeValue * Double(domain.count - 1)
228238
let closestIndex = Int(rangeValueExpandedToCountDomain.rounded())
229239
return domain[closestIndex]
230240
}

Sources/SwiftVizScale/PowerScale.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,12 @@ public struct PowerScale<InputType: ConvertibleWithDouble & NiceValue, OutputTyp
219219
let powDomainLower = pow(domainLower.toDouble(), exponent)
220220
let powDomainHigher = pow(domainHigher.toDouble(), exponent)
221221
let normalizedValueOnPowDomain = normalize(powDomainValue, lower: powDomainLower, higher: powDomainHigher)
222-
let valueMappedToRange = interpolate(normalizedValueOnPowDomain, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
222+
let valueMappedToRange: Double
223+
if reversed {
224+
valueMappedToRange = interpolate(normalizedValueOnPowDomain, lower: rangeHigher.toDouble(), higher: rangeLower.toDouble())
225+
} else {
226+
valueMappedToRange = interpolate(normalizedValueOnPowDomain, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
227+
}
223228
return OutputType.fromDouble(valueMappedToRange)
224229
}
225230

@@ -259,7 +264,12 @@ public struct PowerScale<InputType: ConvertibleWithDouble & NiceValue, OutputTyp
259264
let normalizedRangeValue = normalize(rangeValue.toDouble(), lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
260265
let powDomainLower = pow(domainLower.toDouble(), exponent)
261266
let powDomainHigher = pow(domainHigher.toDouble(), exponent)
262-
let linearInterpolatedValue = interpolate(Double(normalizedRangeValue), lower: powDomainLower, higher: powDomainHigher)
267+
let linearInterpolatedValue: Double
268+
if reversed {
269+
linearInterpolatedValue = interpolate(Double(normalizedRangeValue), lower: powDomainHigher, higher: powDomainLower)
270+
} else {
271+
linearInterpolatedValue = interpolate(Double(normalizedRangeValue), lower: powDomainLower, higher: powDomainHigher)
272+
}
263273
let domainValue = pow(linearInterpolatedValue, 1.0 / exponent)
264274
let downcastDomainValue = InputType.fromDouble(domainValue)
265275
return transformAgainstDomain(downcastDomainValue)

Sources/SwiftVizScale/RadialScale.swift

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,12 @@ public struct RadialScale<InputType: ConvertibleWithDouble & NiceValue, OutputTy
195195
return nil
196196
}
197197
let normalizedInput = normalize(domainValue.toDouble(), lower: domainLower.toDouble(), higher: domainHigher.toDouble())
198-
let result: Double = interpolate(normalizedInput, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
198+
let result: Double
199+
if reversed {
200+
result = interpolate(normalizedInput, lower: rangeHigher.toDouble(), higher: rangeLower.toDouble())
201+
} else {
202+
result = interpolate(normalizedInput, lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
203+
}
199204
return OutputType.fromDouble(result * result)
200205
}
201206

@@ -234,7 +239,12 @@ public struct RadialScale<InputType: ConvertibleWithDouble & NiceValue, OutputTy
234239
}
235240
// inverts the scale, taking a value in the output range and returning the relevant value from the input domain
236241
let normalizedRangeValue = normalize(sqrt(rangeValue.toDouble()), lower: rangeLower.toDouble(), higher: rangeHigher.toDouble())
237-
let mappedToDomain = interpolate(normalizedRangeValue, lower: domainLower.toDouble(), higher: domainHigher.toDouble())
242+
let mappedToDomain: Double
243+
if reversed {
244+
mappedToDomain = interpolate(normalizedRangeValue, lower: domainHigher.toDouble(), higher: domainLower.toDouble())
245+
} else {
246+
mappedToDomain = interpolate(normalizedRangeValue, lower: domainLower.toDouble(), higher: domainHigher.toDouble())
247+
}
238248
let castToInputType = InputType.fromDouble(mappedToDomain)
239249
return transformAgainstDomain(castToInputType)
240250
}

Tests/SwiftVizScaleTests/BandScaleTests.swift

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -215,4 +215,48 @@ class BandScaleTests: XCTestCase {
215215
let scale = BandScale<String, CGFloat>(["1", "2", "3"])
216216
XCTAssertEqual("\(scale)", "band[\"1\", \"2\", \"3\"]->[nil:nil]")
217217
}
218+
219+
func testReversedRangeModifiers() {
220+
var scale = BandScale<String, CGFloat>(["X", "Y", "Z"]).range(0 ... 20)
221+
XCTAssertEqual(scale.reversed, false)
222+
scale = BandScale<String, CGFloat>(["X", "Y", "Z"], reversed: true, from: 0, to: 30)
223+
XCTAssertEqual(scale.reversed, true)
224+
scale = scale.range(0 ... 40)
225+
XCTAssertEqual(scale.reversed, true)
226+
scale = scale.range(reversed: false, 0 ... 40)
227+
XCTAssertEqual(scale.reversed, false)
228+
}
229+
230+
func testReversedCalculations() {
231+
let reversed = BandScale<String, CGFloat>(["X", "Y", "Z"], reversed: true, from: 0, to: 30)
232+
// print(reversed.scale("X"))
233+
assertBand(reversed.scale("Z"), "Z", low: 0, high: 10)
234+
assertBand(reversed.scale("Y"), "Y", low: 10, high: 20)
235+
assertBand(reversed.scale("X"), "X", low: 20, high: 30)
236+
XCTAssertEqual(reversed.invert(15), "Y")
237+
238+
let forward = reversed.range(reversed: false, lower: 0, higher: 30) // identity
239+
assertBand(forward.scale("X"), "X", low: 0, high: 10)
240+
assertBand(forward.scale("Y"), "Y", low: 10, high: 20)
241+
assertBand(forward.scale("Z"), "Z", low: 20, high: 30)
242+
// verify invert
243+
XCTAssertEqual(forward.invert(5), "X")
244+
}
245+
246+
func testReversedTicks() {
247+
let reversed = BandScale<String, CGFloat>(["X", "Y", "Z"], reversed: true, from: 0, to: 30)
248+
let reverseTicks = reversed.ticks(rangeLower: 0, rangeHigher: 30)
249+
XCTAssertEqual(reverseTicks.count, 3)
250+
print(reverseTicks)
251+
assertTick(reverseTicks[0], "X", 25)
252+
assertTick(reverseTicks[1], "Y", 15)
253+
assertTick(reverseTicks[2], "Z", 5)
254+
255+
let forward = reversed.range(reversed: false, lower: 0, higher: 30) // identity
256+
let forwardTicks = forward.ticks(rangeLower: 0, rangeHigher: 30)
257+
XCTAssertEqual(forwardTicks.count, 3)
258+
assertTick(forwardTicks[0], "X", 5)
259+
assertTick(forwardTicks[1], "Y", 15)
260+
assertTick(forwardTicks[2], "Z", 25)
261+
}
218262
}

Tests/SwiftVizScaleTests/LinearScaleTests.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,4 +308,40 @@ final class LinearScaleTests: XCTestCase {
308308
scale = scale.range(reversed: false, 0 ... 40)
309309
XCTAssertEqual(scale.reversed, false)
310310
}
311+
312+
func testReversedCalculations() {
313+
let scale = LinearScale<Double, CGFloat>(20, reversed: true, rangeLower: 0, rangeHigher: 20)
314+
XCTAssertEqual(scale.scale(0), 20)
315+
XCTAssertEqual(scale.scale(20), 0)
316+
XCTAssertEqual(scale.scale(5), 15)
317+
// verify invert
318+
XCTAssertEqual(scale.invert(15), 5)
319+
320+
let forward = scale.range(reversed: false, lower: 0, higher: 20) // identity
321+
XCTAssertEqual(forward.scale(0), 0)
322+
XCTAssertEqual(forward.scale(20), 20)
323+
XCTAssertEqual(forward.scale(5), 5)
324+
// verify invert
325+
XCTAssertEqual(forward.invert(5), 5)
326+
}
327+
328+
func testReversedTicks() {
329+
let reversed = LinearScale<Double, CGFloat>(20, reversed: true, rangeLower: 0, rangeHigher: 20)
330+
let reverseTicks = reversed.ticks(rangeLower: 0, rangeHigher: 20)
331+
XCTAssertEqual(reverseTicks.count, 5)
332+
assertTick(reverseTicks[0], "0.0", 20)
333+
assertTick(reverseTicks[1], "5.0", 15)
334+
assertTick(reverseTicks[2], "10.0", 10)
335+
assertTick(reverseTicks[3], "15.0", 5)
336+
assertTick(reverseTicks[4], "20.0", 0)
337+
338+
let forward = reversed.range(reversed: false, lower: 0, higher: 20) // identity
339+
let forwardTicks = forward.ticks(rangeLower: 0, rangeHigher: 20)
340+
XCTAssertEqual(forwardTicks.count, 5)
341+
assertTick(forwardTicks[0], "0.0", 0)
342+
assertTick(forwardTicks[1], "5.0", 5)
343+
assertTick(forwardTicks[2], "10.0", 10)
344+
assertTick(forwardTicks[3], "15.0", 15)
345+
assertTick(forwardTicks[4], "20.0", 20)
346+
}
311347
}

Tests/SwiftVizScaleTests/LogScaleTests.swift

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,4 +289,51 @@ class LogScaleTests: XCTestCase {
289289
XCTAssertEqual(updated.domainLower, Double.leastNonzeroMagnitude)
290290
XCTAssertEqual(updated.domainHigher, 5)
291291
}
292+
293+
func testReversedRangeModifiers() {
294+
var scale = LogScale<Double, CGFloat>(1 ... 100).range(1 ... 100)
295+
XCTAssertEqual(scale.reversed, false)
296+
scale = LogScale(from: 1, to: 100, reversed: true, rangeLower: 1, rangeHigher: 100)
297+
XCTAssertEqual(scale.reversed, true)
298+
scale = scale.range(0 ... 40)
299+
XCTAssertEqual(scale.reversed, true)
300+
scale = scale.range(reversed: false, 0 ... 40)
301+
XCTAssertEqual(scale.reversed, false)
302+
}
303+
304+
func testReversedCalculations() {
305+
let scale = LogScale<Double, CGFloat>(from: 1, to: 100, reversed: true, rangeLower: 1, rangeHigher: 100)
306+
XCTAssertEqual(scale.scale(1), 100)
307+
XCTAssertEqual(scale.scale(100), 1)
308+
XCTAssertEqual(scale.scale(10), 50.5)
309+
// verify invert
310+
XCTAssertEqual(scale.invert(50.5), 10)
311+
312+
let forward = scale.range(reversed: false, lower: 1, higher: 100) // log identity
313+
XCTAssertEqual(forward.scale(1), 1)
314+
XCTAssertEqual(forward.scale(100), 100)
315+
XCTAssertEqual(forward.scale(10), 50.5)
316+
// verify invert
317+
XCTAssertEqual(forward.invert(50.5), 10)
318+
}
319+
320+
func testReversedTicks() {
321+
let reversed = LogScale<Double, CGFloat>(from: 1, to: 100, reversed: true, rangeLower: 1, rangeHigher: 100)
322+
let reverseTicks = reversed.ticks(rangeLower: 0, rangeHigher: 20)
323+
XCTAssertEqual(reverseTicks.count, 5)
324+
assertTick(reverseTicks[0], "20.0", 6.9897)
325+
assertTick(reverseTicks[1], "40.0", 3.9794)
326+
assertTick(reverseTicks[2], "60.0", 2.2185)
327+
assertTick(reverseTicks[3], "80.0", 0.9691)
328+
assertTick(reverseTicks[4], "100.0", 0)
329+
330+
let forward = reversed.range(reversed: false, lower: 0, higher: 20) // identity
331+
let forwardTicks = forward.ticks(rangeLower: 0, rangeHigher: 20)
332+
XCTAssertEqual(forwardTicks.count, 5)
333+
assertTick(forwardTicks[0], "20.0", 13.010)
334+
assertTick(forwardTicks[1], "40.0", 16.020)
335+
assertTick(forwardTicks[2], "60.0", 17.781)
336+
assertTick(forwardTicks[3], "80.0", 19.030)
337+
assertTick(forwardTicks[4], "100.0", 20)
338+
}
292339
}

0 commit comments

Comments
 (0)