Skip to content

Commit 941986e

Browse files
committed
Added some docs and utilities for ColorMap, provenance info for ColorMapData.
1 parent 538b688 commit 941986e

File tree

2 files changed

+2030
-1924
lines changed

2 files changed

+2030
-1924
lines changed

Sources/SwiftPlot/ColorMap.swift

Lines changed: 176 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,122 +1,195 @@
11

2-
// This struct is a namespace for static functions and properties which return known color maps.
3-
// All of these functions and properties also _return_ a `ColorMap`,
4-
// instances of which are used as an interface type
5-
// rather than existentials of `ColorMapProtocol` directly
6-
// (again, so users can have access to convenient, known color maps).
7-
2+
/// An object which maps a value in the range `0...1` to a `Color`.
3+
///
4+
/// This object wraps a conformer to `ColorMapProtocol`, and is used instead of `ColorMapProtocol` directly
5+
/// in order to provide a cleaner interface and to automatically clamp out-of-range values.
6+
/// Common color maps are available as static functions on this type, allowing code such as:
7+
/// `x.colorMap = .fiveColorHeatMap`.
8+
///
89
public struct ColorMap: ColorMapProtocol {
9-
var base: ColorMapProtocol
10-
11-
public init(_ base: ColorMapProtocol) {
12-
if let existing = base as? ColorMap { self = existing }
13-
else { self.base = base }
14-
}
15-
16-
public func colorForOffset(_ offset: Float) -> Color {
17-
return base.colorForOffset(offset)
18-
}
10+
var base: ColorMapProtocol
11+
12+
public init(_ base: ColorMapProtocol) {
13+
// If base is already wrapped, do not re-wrap it.
14+
if let existing = base as? ColorMap { self = existing }
15+
else { self.base = base }
16+
}
17+
public func colorForOffset(_ offset: Double) -> Color {
18+
let offset = min(max(offset, 0), 1)
19+
return base.colorForOffset(offset)
20+
}
1921
}
2022

23+
/// An object which maps a value in the range `0...1` to a `Color`.
24+
///
2125
public protocol ColorMapProtocol {
22-
func colorForOffset(_ offset: Float) -> Color
26+
func colorForOffset(_ offset: Double) -> Color
27+
}
28+
extension ColorMapProtocol {
29+
func colorForOffset(_ offset: Float) -> Color {
30+
return colorForOffset(Double(offset))
31+
}
2332
}
2433

25-
// Linear Gradients.
34+
// MARK: - Color Transformations.
2635

27-
struct LinearGradient: ColorMapProtocol {
28-
struct Stop {
29-
var color: Color
30-
var position: Double
36+
private struct ColorTransformer: ColorMapProtocol {
37+
var base: ColorMapProtocol
38+
var transform: (Color)->Color
39+
init(_ base: ColorMapProtocol, transform: @escaping (Color)->Color) {
40+
self.base = base; self.transform = transform
41+
}
42+
func colorForOffset(_ offset: Double) -> Color {
43+
return transform(base.colorForOffset(offset))
44+
}
45+
}
46+
47+
extension ColorMap {
48+
49+
/// Returns a `ColorMap` whose output is transformed by the given closure.
50+
///
51+
public func withTransform(_ transform: @escaping (Color)->Color) -> ColorMap {
52+
return ColorMap(ColorTransformer(base, transform: transform))
53+
}
54+
55+
/// Returns a `ColorMap` whose output colors' alpha components are given by `alpha`.
56+
///
57+
public func withAlpha(_ alpha: Float) -> ColorMap {
58+
return withTransform { $0.withAlpha(alpha) }
59+
}
60+
61+
/// Returns a `ColorMap` whose output colors are lightened by the given `amount`.
62+
///
63+
public func lightened(by amount: Float) -> ColorMap {
64+
return withTransform { $0.linearBlend(with: .white, offset: amount) }
65+
}
66+
67+
/// Returns a `ColorMap` whose output colors are darkened by the given `amount`.
68+
///
69+
public func darkened(by amount: Float) -> ColorMap {
70+
return withTransform { $0.linearBlend(with: .black, offset: amount) }
71+
}
72+
}
73+
74+
// MARK: - Offset Transformations.
75+
76+
private struct ColorMapOffsetTransformer: ColorMapProtocol {
77+
var base: ColorMapProtocol
78+
var transform: (Double)->Double
79+
init(_ base: ColorMapProtocol, transform: @escaping (Double)->Double) {
80+
self.base = base; self.transform = transform
81+
}
82+
func colorForOffset(_ offset: Double) -> Color {
83+
// Ensure that we don't transform the offset out of bounds.
84+
var transformedOffset = transform(offset)
85+
transformedOffset = min(max(transformedOffset, 0), 1)
86+
return base.colorForOffset(transformedOffset)
87+
}
88+
}
89+
90+
extension ColorMap {
3191

32-
init(_ color: Color, at pos: Double) {
33-
self.color = color; self.position = pos
34-
}
35-
}
36-
37-
var stops: [Stop]
38-
39-
init(stops: [Stop]) {
40-
self.stops = stops.sorted { $0.position < $1.position }
41-
}
42-
43-
init(start: Color, end: Color) {
44-
self.init(stops: [Stop(start, at: 0), Stop(end, at: 1)])
45-
}
46-
47-
func colorForOffset(_ offset: Float) -> Color {
48-
return colorForOffset(Double(offset))
49-
}
50-
51-
func colorForOffset(_ offset: Double) -> Color {
52-
guard (0...1).contains(offset),
53-
let rightStopIdx = stops.firstIndex(where: { $0.position > offset }) else {
54-
return stops.last?.color ?? .black
55-
}
56-
let rightStop = stops[rightStopIdx]
57-
guard rightStopIdx > stops.startIndex else { return rightStop.color }
58-
let leftStop = stops[stops.index(before: rightStopIdx)]
59-
assert(leftStop.position <= offset)
60-
61-
let distance = rightStop.position - leftStop.position
62-
guard distance > 0 else { return rightStop.color }
92+
private func withOffsetTransform(_ transform: @escaping (Double) -> Double) -> ColorMap {
93+
return ColorMap(ColorMapOffsetTransformer(base, transform: transform))
94+
}
6395

64-
let offset = Float((offset - leftStop.position) / distance)
65-
return leftStop.color.linearBlend(with: rightStop.color, offset: offset)
66-
}
96+
/// Returns a `ColorMap` whose output at offset `x` is equal to this `ColorMap`'s output at `1 - x`.
97+
///
98+
public func reversed() -> ColorMap {
99+
return withOffsetTransform { 1 - $0 }
100+
}
101+
}
102+
103+
// MARK: - Single Colors.
104+
105+
private struct SingleColorMap: ColorMapProtocol {
106+
var color: Color
107+
func colorForOffset(_ offset: Double) -> Color {
108+
return color
109+
}
67110
}
68111

69112
extension ColorMap {
70-
71-
public static func linear(_ start: Color, _ end: Color) -> ColorMap {
72-
return ColorMap(LinearGradient(start: start, end: end))
73-
}
74-
75-
public static let fiveColorHeatMap = ColorMap(LinearGradient(stops: [
76-
LinearGradient.Stop(Color(0, 0, 1, 1), at: 0),
77-
LinearGradient.Stop(Color(0, 1, 1, 1), at: 0.25),
78-
LinearGradient.Stop(Color(0, 1, 0, 1), at: 0.5),
79-
LinearGradient.Stop(Color(1, 1, 0, 1), at: 0.75),
80-
LinearGradient.Stop(Color(1, 0, 0, 1), at: 1),
81-
]))
82-
83-
public static let sevenColorHeatMap = ColorMap(LinearGradient(stops: [
84-
LinearGradient.Stop(Color(0, 0, 0, 1), at: 0),
85-
LinearGradient.Stop(Color(0, 0, 1, 1), at: 0.1666666667),
86-
LinearGradient.Stop(Color(0, 1, 1, 1), at: 0.3333333333),
87-
LinearGradient.Stop(Color(0, 1, 0, 1), at: 0.5),
88-
LinearGradient.Stop(Color(1, 1, 0, 1), at: 0.6666666667),
89-
LinearGradient.Stop(Color(1, 0, 0, 1), at: 0.8333333333),
90-
LinearGradient.Stop(Color(1, 1, 1, 1), at: 1)
91-
]))
113+
114+
/// Returns a `ColorMap` which always returns the same color.
115+
///
116+
public static func color(_ color: Color) -> ColorMap {
117+
return ColorMap(SingleColorMap(color: color))
118+
}
119+
}
120+
121+
// MARK: - Linear Gradients.
122+
123+
/// A position along a gradient.
124+
///
125+
public struct GradientStop {
126+
public var color: Color
127+
public var position: Double
128+
public init(_ color: Color, at position: Double) {
129+
self.color = color; self.position = position
130+
}
92131
}
93132

94-
// Transforming.
95-
96-
struct ColorTransformer<T>: ColorMapProtocol where T: ColorMapProtocol {
97-
var base: T
98-
var transform: (inout Color)->Void
99-
init(_ base: T, transform: @escaping (inout Color)->Void) {
100-
self.base = base; self.transform = transform
101-
}
102-
func colorForOffset(_ offset: Float) -> Color {
103-
var color = base.colorForOffset(offset)
104-
transform(&color)
105-
return color
106-
}
133+
private struct LinearGradient: ColorMapProtocol {
134+
var stops: [GradientStop]
135+
136+
init(stops: [GradientStop]) {
137+
self.stops = stops.sorted { $0.position < $1.position }
138+
}
139+
init(start: Color, end: Color) {
140+
self.init(stops: [GradientStop(start, at: 0), GradientStop(end, at: 1)])
141+
}
142+
func colorForOffset(_ offset: Double) -> Color {
143+
guard let rightStopIdx = stops.firstIndex(where: { $0.position > offset }) else {
144+
return stops.last?.color ?? .black
145+
}
146+
let rightStop = stops[rightStopIdx]
147+
guard rightStopIdx > stops.startIndex else { return rightStop.color }
148+
let leftStop = stops[stops.index(before: rightStopIdx)]
149+
assert(leftStop.position <= offset)
150+
151+
let distance = rightStop.position - leftStop.position
152+
guard distance > 0 else { return rightStop.color }
153+
154+
let offset = (offset - leftStop.position) / distance
155+
return leftStop.color.linearBlend(with: rightStop.color, offset: Float(offset))
156+
}
107157
}
108158

109159
extension ColorMap {
110-
111-
public func withAlpha(_ alpha: Float) -> ColorMap {
112-
return ColorMap(ColorTransformer(self) { $0.a = alpha })
113-
}
114-
115-
public func lightened(by: Float) -> ColorMap {
116-
return ColorMap(ColorTransformer(self) { $0 = $0.linearBlend(with: .white, offset: by) })
117-
}
118-
119-
public func darkened(by: Float) -> ColorMap {
120-
return ColorMap(ColorTransformer(self) { $0 = $0.linearBlend(with: .black, offset: by) })
121-
}
160+
161+
/// Returns a `ColorMap` whose output is a linear gradient with the given stops.
162+
///
163+
public static func linearGradient(_ stops: [GradientStop]) -> ColorMap {
164+
return ColorMap(LinearGradient(stops: stops))
165+
}
166+
167+
/// Returns a `ColorMap` whose output is a linear gradient between the given colors.
168+
///
169+
public static func linearGradient(_ start: Color, _ end: Color) -> ColorMap {
170+
return ColorMap(LinearGradient(start: start, end: end))
171+
}
172+
173+
/// A standard, five-color heat map.
174+
///
175+
public static let fiveColorHeatMap = ColorMap.linearGradient([
176+
GradientStop(Color(0, 0, 1, 1), at: 0),
177+
GradientStop(Color(0, 1, 1, 1), at: 0.25),
178+
GradientStop(Color(0, 1, 0, 1), at: 0.5),
179+
GradientStop(Color(1, 1, 0, 1), at: 0.75),
180+
GradientStop(Color(1, 0, 0, 1), at: 1),
181+
])
182+
183+
/// A standard, seven-color heat map.
184+
///
185+
public static let sevenColorHeatMap = ColorMap.linearGradient([
186+
GradientStop(Color(0, 0, 0, 1), at: 0),
187+
GradientStop(Color(0, 0, 1, 1), at: 0.1666666667),
188+
GradientStop(Color(0, 1, 1, 1), at: 0.3333333333),
189+
GradientStop(Color(0, 1, 0, 1), at: 0.5),
190+
GradientStop(Color(1, 1, 0, 1), at: 0.6666666667),
191+
GradientStop(Color(1, 0, 0, 1), at: 0.8333333333),
192+
GradientStop(Color(1, 1, 1, 1), at: 1)
193+
])
122194
}
195+

0 commit comments

Comments
 (0)