Skip to content

Commit 560734c

Browse files
committed
fiddling with bringing in hex sequences of colors, pulled from D3 patterns
1 parent 7429642 commit 560734c

File tree

3 files changed

+163
-1
lines changed

3 files changed

+163
-1
lines changed

Sources/SwiftVizScale/ArrayInterpolator.swift

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,16 @@ import CoreGraphics
1818
public struct ArrayInterpolator {
1919
// Color pallets for interpolation and presentation:
2020
// https://github.com/d3/d3-scale-chromatic/blob/main/README.md
21+
// https://bids.github.io/colormap/
2122
var colors: [CGColor]
2223

24+
/// Returns an index and re-normalized interpolation value to be able to step-wise interpolate through an array of options.
25+
/// - Parameters:
26+
/// - t: The unit value to interpolate into and between the array elements
27+
/// - count: The number of elements in the array
2328
static func interpolateIntoSteps(_ t: Double, _ count: Int) -> (Int, Double) {
2429
precondition(t >= 0 && t <= 1)
2530
precondition(count > 1)
26-
2731
// Calculate the step size for each index evenly dividing the space by the number
2832
// of elements.
2933
let step = 1.0 / Double(count - 1)
@@ -67,12 +71,39 @@ public struct ArrayInterpolator {
6771
self.colors = colors
6872
}
6973

74+
public init(_ hexSequence: String) {
75+
precondition(hexSequence.count >= 12)
76+
let colors = CGColor.fromHexSequence(hexSequence)
77+
self.init(colors)
78+
}
79+
7080
static let white = CGColor(srgbRed: 1, green: 1, blue: 1, alpha: 1)
7181
static let black = CGColor(srgbRed: 0, green: 0, blue: 0, alpha: 1)
7282
static let red = CGColor(srgbRed: 1, green: 0, blue: 0, alpha: 1)
7383
static let blue = CGColor(srgbRed: 0, green: 0, blue: 1, alpha: 1)
7484
static let green = CGColor(srgbRed: 0, green: 1, blue: 0, alpha: 1)
7585

86+
// MARK: - Diverging Color Schemes, replicated from d3-scale-chromatic
87+
88+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/BrBG.js
89+
public static let BrBG = Self("5430058c510abf812ddfc27df6e8c3f5f5f5c7eae580cdc135978f01665e003c30")
90+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/PRGn.js
91+
public static let PrGN = Self("40004b762a839970abc2a5cfe7d4e8f7f7f7d9f0d3a6dba05aae611b783700441b")
92+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/PiYG.js
93+
public static let PiYG = Self("8e0152c51b7dde77aef1b6dafde0eff7f7f7e6f5d0b8e1867fbc414d9221276419")
94+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/PuOr.js
95+
public static let PuOR = Self("2d004b5427888073acb2abd2d8daebf7f7f7fee0b6fdb863e08214b358067f3b08")
96+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/RdBu.js
97+
public static let RdBu = Self("67001fb2182bd6604df4a582fddbc7f7f7f7d1e5f092c5de4393c32166ac053061")
98+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/RdGy.js
99+
public static let RdGy = Self("67001fb2182bd6604df4a582fddbc7ffffffe0e0e0bababa8787874d4d4d1a1a1a")
100+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/RdYlBu.js
101+
public static let RdYlBu = Self("a50026d73027f46d43fdae61fee090ffffbfe0f3f8abd9e974add14575b4313695")
102+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/RdYlGn.js
103+
public static let RdYlGn = Self("a50026d73027f46d43fdae61fee08bffffbfd9ef8ba6d96a66bd631a9850006837")
104+
// https://github.com/d3/d3-scale-chromatic/blob/main/src/diverging/Spectral.js
105+
public static let Spectral = Self("9e0142d53e4ff46d43fdae61fee08bffffbfe6f598abdda466c2a53288bd5e4fa2")
106+
76107
/// An interpolator that maps to various shades between white to black.
77108
public static let Grays = Self(white, black)
78109
/// An interpolator that maps to various shades between white to blue.
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//
2+
// CGColor+hex.swift
3+
//
4+
5+
import CoreGraphics
6+
import Foundation
7+
8+
extension CGColor {
9+
func fromHex(_ hex: String) -> Self? {
10+
let r, g, b, a: CGFloat
11+
12+
if hex.hasPrefix("#") {
13+
let start = hex.index(hex.startIndex, offsetBy: 1)
14+
let hexColor = String(hex[start...])
15+
16+
if hexColor.count == 8 {
17+
let scanner = Scanner(string: hexColor)
18+
var hexNumber: UInt64 = 0
19+
20+
if scanner.scanHexInt64(&hexNumber) {
21+
r = CGFloat((hexNumber & 0xFF00_0000) >> 24) / 255
22+
g = CGFloat((hexNumber & 0x00FF_0000) >> 16) / 255
23+
b = CGFloat((hexNumber & 0x0000_FF00) >> 8) / 255
24+
a = CGFloat(hexNumber & 0x0000_00FF) / 255
25+
26+
return type(of: self).init(srgbRed: r, green: g, blue: b, alpha: a)
27+
}
28+
}
29+
if hexColor.count == 6 {
30+
let scanner = Scanner(string: hexColor)
31+
var hexNumber: UInt64 = 0
32+
33+
if scanner.scanHexInt64(&hexNumber) {
34+
r = CGFloat((hexNumber & 0x00FF_0000) >> 16) / 255
35+
g = CGFloat((hexNumber & 0x0000_FF00) >> 8) / 255
36+
b = CGFloat(hexNumber & 0x0000_00FF) / 255
37+
38+
return type(of: self).init(srgbRed: r, green: g, blue: b, alpha: 1.0)
39+
}
40+
}
41+
}
42+
return nil
43+
}
44+
45+
static func fromHexSequence(_ hex: String) -> [CGColor] {
46+
var colors: [CGColor] = []
47+
48+
var start = hex.startIndex
49+
while start <= hex.endIndex {
50+
if let end = hex.index(start, offsetBy: 5, limitedBy: hex.endIndex) {
51+
let slice = String(hex[start ... end])
52+
let scanner = Scanner(string: slice)
53+
var hexNumber: UInt64 = 0
54+
if scanner.scanHexInt64(&hexNumber) {
55+
let r, g, b: CGFloat
56+
r = CGFloat((hexNumber & 0x00FF_0000) >> 16) / 255
57+
// print(CGFloat((hexNumber & 0x00ff0000) >> 16))
58+
g = CGFloat((hexNumber & 0x0000_FF00) >> 8) / 255
59+
// print(CGFloat((hexNumber & 0x0000ff00) >> 8))
60+
b = CGFloat(hexNumber & 0x0000_00FF) / 255
61+
// print(CGFloat(hexNumber & 0x000000ff))
62+
colors.append(CGColor(srgbRed: r, green: g, blue: b, alpha: 1.0))
63+
}
64+
start = hex.index(start, offsetBy: 6)
65+
} else {
66+
// The index increment to get the next slice wasn't able to get an additional
67+
// 6 characters, so quit and be done with what we've captured.
68+
break
69+
}
70+
}
71+
return colors
72+
}
73+
74+
// MARK: - Computed Properties
75+
76+
var toHex: String? {
77+
toHex()
78+
}
79+
80+
// MARK: - From CGColor to String
81+
82+
func toHex(alpha: Bool = false) -> String? {
83+
guard let components = components, components.count >= 3 else {
84+
return nil
85+
}
86+
87+
let r = Float(components[0])
88+
let g = Float(components[1])
89+
let b = Float(components[2])
90+
var a = Float(1.0)
91+
92+
if components.count >= 4 {
93+
a = Float(components[3])
94+
}
95+
96+
if alpha {
97+
return String(format: "%02lX%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255), lroundf(a * 255))
98+
} else {
99+
return String(format: "%02lX%02lX%02lX", lroundf(r * 255), lroundf(g * 255), lroundf(b * 255))
100+
}
101+
}
102+
}

Tests/SwiftVizScaleTests/ArrayInterpolatorTests.swift

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,35 @@ final class ArrayInterpolatorTests: XCTestCase {
1717
}
1818
}
1919

20+
func testHexValuesFromInterpolatorGuides() throws {
21+
XCTAssertEqual(ArrayInterpolator.white.toHex(), "FFFFFF")
22+
XCTAssertEqual(ArrayInterpolator.black.toHex(), "000000")
23+
XCTAssertEqual(ArrayInterpolator.red.toHex(), "FF0000")
24+
XCTAssertEqual(ArrayInterpolator.green.toHex(), "00FF00")
25+
XCTAssertEqual(ArrayInterpolator.blue.toHex(), "0000FF")
26+
}
27+
28+
func testHexSequence() throws {
29+
let colors = CGColor.fromHexSequence("010101FFFFFF")
30+
XCTAssertEqual(colors.count, 2)
31+
XCTAssertEqual(colors[0].toHex(), "010101")
32+
XCTAssertEqual(colors[1].toHex(), "FFFFFF")
33+
}
34+
35+
func testHexSequenceWithBrokenSequence() throws {
36+
let colors = CGColor.fromHexSequence("010101FFFFFFABCD")
37+
XCTAssertEqual(colors.count, 2)
38+
XCTAssertEqual(colors[0].toHex(), "010101")
39+
XCTAssertEqual(colors[1].toHex(), "FFFFFF")
40+
}
41+
42+
func testHexSequenceWithInvalidSequence() throws {
43+
let colors = CGColor.fromHexSequence("010101FFFFFFgoodbye")
44+
XCTAssertEqual(colors.count, 2)
45+
XCTAssertEqual(colors[0].toHex(), "010101")
46+
XCTAssertEqual(colors[1].toHex(), "FFFFFF")
47+
}
48+
2049
func testFiveStepInterpolationValues() throws {
2150
// Five colors added means there'll be four breaks:
2251
// 0, 0.25, 0.5, 0.75, and 1.0

0 commit comments

Comments
 (0)