Skip to content

Commit 3cdffbb

Browse files
committed
- Dropped the Comparable requirement down in to a comparator function on Interpolator. This allows us to Heatmap sequences of arbitrary types (for example, using a KeyPath to an int or float to grade them).
- Moved Interpolator to its own file. It should possibly be given a new name, too. - Added convenience functions for generating a Heatmap from arbitrary RACs or 2D Sequences. This allows for some really cool things.
1 parent a1e64d5 commit 3cdffbb

File tree

3 files changed

+181
-111
lines changed

3 files changed

+181
-111
lines changed

Sources/SwiftPlot/Heatmap.swift

Lines changed: 82 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
11

2-
public struct Heatmap<SeriesType>
3-
where SeriesType: Sequence, SeriesType.Element: Sequence,
4-
SeriesType.Element.Element: Comparable {
2+
// Todo list for Heatmap:
3+
// - Colormaps
4+
// - Shift grid to block bounds
5+
// - Draw grid over blocks?
6+
// - Spacing between blocks
7+
// - Setting X/Y axis labels
8+
// - Displaying colormap next to plot
9+
// - Collection slicing by filter closure
10+
11+
/// A heatmap is a plot of 2-dimensional data, where each value is assigned a colour value along a gradient.
12+
///
13+
/// Use the `interpolator` property to control how values are graded. For example, if your data structure has
14+
/// a salient integer or floating-point property, `Interpolator.linearByKeyPath` will allow you to grade values by that property.
15+
public struct Heatmap<SeriesType> where SeriesType: Sequence, SeriesType.Element: Sequence {
516

617
public typealias Element = SeriesType.Element.Element
718

819
public var layout = GraphLayout()
20+
921
public var values: SeriesType
1022
public var interpolator: Interpolator<Element>
1123

@@ -17,50 +29,13 @@ where SeriesType: Sequence, SeriesType.Element: Sequence,
1729
}
1830
}
1931

20-
// Initialisers with default arguments.
21-
22-
extension Heatmap
23-
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
24-
SeriesType.ArrayLiteralElement == SeriesType.Element {
25-
26-
public init(interpolator: Interpolator<Element>) {
27-
self.init(values: [[]], interpolator: interpolator)
28-
}
29-
}
30-
31-
extension Heatmap
32-
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
33-
SeriesType.ArrayLiteralElement == SeriesType.Element, Element: FloatConvertible {
34-
35-
public init(values: SeriesType) {
36-
self.init(values: values, interpolator: .linear)
37-
}
38-
39-
public init() {
40-
self.init(interpolator: .linear)
41-
}
42-
}
43-
44-
extension Heatmap
45-
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
46-
SeriesType.ArrayLiteralElement == SeriesType.Element, Element: FixedWidthInteger {
47-
48-
public init(values: SeriesType) {
49-
self.init(values: values, interpolator: .linear)
50-
}
51-
52-
public init() {
53-
self.init(interpolator: .linear)
54-
}
55-
}
56-
5732
// Layout and drawing.
5833

5934
extension Heatmap: HasGraphLayout, Plot {
6035

6136
public struct DrawingData {
6237
var values: SeriesType?
63-
var range: ClosedRange<Element>?
38+
var range: (min: Element, max: Element)?
6439
var itemSize = Size.zero
6540
var rows = 0
6641
var columns = 0
@@ -70,7 +45,6 @@ extension Heatmap: HasGraphLayout, Plot {
7045

7146
var results = DrawingData()
7247
var markers = PlotMarkers()
73-
7448
// Extract the first (inner) element as a starting point.
7549
guard let firstElem = values.first(where: { _ in true })?.first(where: { _ in true }) else {
7650
return (results, nil)
@@ -83,16 +57,16 @@ extension Heatmap: HasGraphLayout, Plot {
8357
for row in values {
8458
var columnsInRow = 0
8559
for column in row {
86-
maxValue = max(maxValue, column)
87-
minValue = min(minValue, column)
60+
maxValue = interpolator.compare(maxValue, column) ? column : maxValue
61+
minValue = interpolator.compare(minValue, column) ? minValue : column
8862
columnsInRow += 1
8963
}
9064
maxColumns = max(maxColumns, columnsInRow)
9165
totalRows += 1
9266
}
9367
// Update results.
9468
results.values = values
95-
results.range = minValue...maxValue
69+
results.range = (minValue, maxValue)
9670
results.rows = totalRows
9771
results.columns = maxColumns
9872
results.itemSize = Size(
@@ -116,20 +90,18 @@ extension Heatmap: HasGraphLayout, Plot {
11690
}
11791

11892
public func drawData(_ data: DrawingData, size: Size, renderer: Renderer) {
119-
120-
12193
guard let values = data.values, let range = data.range else { return }
12294

12395
for (rowIdx, row) in values.enumerated() {
124-
for (columnIdx, column) in row.enumerated() {
96+
for (columnIdx, element) in row.enumerated() {
12597
let rect = Rect(
12698
origin: Point(Float(columnIdx) * data.itemSize.width,
12799
Float(rowIdx) * data.itemSize.height),
128-
size: data.itemSize)
129-
renderer.drawSolidRect(rect,
130-
fillColor: getColor(of: column, range: range),
131-
hatchPattern: .none)
132-
// renderer.drawText(text: String(describing: column),
100+
size: data.itemSize
101+
)
102+
let color = getColor(of: element, min: range.min, max: range.max)
103+
renderer.drawSolidRect(rect, fillColor: color, hatchPattern: .none)
104+
// renderer.drawText(text: String(describing: element),
133105
// location: rect.origin + Point(50,50),
134106
// textSize: 20,
135107
// color: .white,
@@ -139,70 +111,82 @@ extension Heatmap: HasGraphLayout, Plot {
139111
}
140112
}
141113

142-
func getColor(of value: Element, range: ClosedRange<Element>) -> Color {
114+
private func getColor(of value: Element, min: Element, max: Element) -> Color {
143115
let startColor = Color.orange
144116
let endColor = Color.purple
145-
let interp = interpolator.callAsFunction(value, in: range)
146-
147-
return lerp(startColor: startColor, endColor: endColor, interp)
117+
let offset = interpolator.interpolate(value, min, max)
118+
return lerp(startColor: startColor, endColor: endColor, offset)
148119
}
149120
}
150121

122+
// Initialisers with default arguments.
151123

152-
// Interpolator.
124+
extension Heatmap
125+
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
126+
SeriesType.ArrayLiteralElement == SeriesType.Element {
127+
128+
public init(interpolator: Interpolator<Element>) {
129+
self.init(values: [[]], interpolator: interpolator)
130+
}
131+
}
153132

154-
public struct Interpolator<Element> where Element: Comparable {
155-
public var interpolate: (Element, ClosedRange<Element>) -> Float
133+
extension Heatmap
134+
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
135+
SeriesType.ArrayLiteralElement == SeriesType.Element, Element: FloatConvertible {
156136

157-
public init(_ block: @escaping (Element, ClosedRange<Element>)->Float) {
158-
self.interpolate = block
137+
public init(values: SeriesType) {
138+
self.init(values: values, interpolator: .linear)
159139
}
160-
public func callAsFunction(_ item: Element, in range: ClosedRange<Element>) -> Float {
161-
interpolate(item, range)
140+
141+
public init() {
142+
self.init(interpolator: .linear)
162143
}
163144
}
164145

165-
extension Interpolator where Element: FloatConvertible {
166-
public static var linear: Interpolator {
167-
Interpolator { value, range in
168-
let value = Float(value)
169-
let range = Float(range.lowerBound)...Float(range.upperBound)
170-
let totalDistance = range.lowerBound.distance(to: range.upperBound)
171-
let valueOffset = range.lowerBound.distance(to: value)
172-
return valueOffset/totalDistance
173-
}
146+
extension Heatmap
147+
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
148+
SeriesType.ArrayLiteralElement == SeriesType.Element, Element: FixedWidthInteger {
149+
150+
public init(values: SeriesType) {
151+
self.init(values: values, interpolator: .linear)
174152
}
175-
}
176-
extension Interpolator where Element: FixedWidthInteger {
177-
public static var linear: Interpolator {
178-
Interpolator { value, range in
179-
let distance = range.lowerBound.distance(to: range.upperBound)
180-
let valDist = range.lowerBound.distance(to: value)
181-
return Float(valDist)/Float(distance)
182-
}
153+
154+
public init() {
155+
self.init(interpolator: .linear)
183156
}
184157
}
185158

186-
extension Interpolator {
159+
// Collection construction shorthand.
160+
161+
extension Sequence where Element: Sequence {
187162

188-
public static func linearByKeyPath<T>(_ kp: KeyPath<Element, T>) -> Interpolator<Element>
189-
where T: FloatConvertible {
190-
let i = Interpolator<T>.linear
191-
return Interpolator { value, range in
192-
let value = value[keyPath: kp]
193-
let range = range.lowerBound[keyPath: kp]...range.upperBound[keyPath: kp]
194-
return i.interpolate(value, range)
195-
}
163+
/// Returns a heatmap of values from this 2-dimensional sequence.
164+
/// - parameters:
165+
/// - interpolator: A function or `KeyPath` which maps values to a continuum between 0 and 1.
166+
/// - returns: A heatmap plot of the sequence's inner items.
167+
public func heatmap(interpolator: Interpolator<Element.Element>) -> Heatmap<Self> {
168+
return Heatmap(values: self, interpolator: interpolator)
196169
}
170+
}
171+
172+
extension RandomAccessCollection {
197173

198-
public static func linearByKeyPath<T>(_ kp: KeyPath<Element, T>) -> Interpolator<Element>
199-
where T: FixedWidthInteger {
200-
let i = Interpolator<T>.linear
201-
return Interpolator { value, range in
202-
let value = value[keyPath: kp]
203-
let range = range.lowerBound[keyPath: kp]...range.upperBound[keyPath: kp]
204-
return i.interpolate(value, range)
174+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
175+
/// - parameters:
176+
/// - width: The width of the heatmap to generate. Must be greater than 0.
177+
/// - interpolator: A function or `KeyPath` which maps values to a continuum between 0 and 1.
178+
/// - returns: A heatmap plot of the collection's values.
179+
public func heatmap(width: Int, interpolator: Interpolator<Element>) -> Heatmap<[SubSequence]> {
180+
precondition(width > 0, "Cannot build a histogram with zero or negative width")
181+
let height = Int((Float(count) / Float(width)).rounded(.up))
182+
return (0..<height).map { row -> SubSequence in
183+
guard let start = index(startIndex, offsetBy: row * width, limitedBy: endIndex) else {
184+
return self[startIndex..<startIndex]
205185
}
186+
guard let end = index(start, offsetBy: width, limitedBy: endIndex) else {
187+
return self[start..<endIndex]
188+
}
189+
return self[start..<end]
190+
}.heatmap(interpolator: interpolator)
206191
}
207-
208192
}

Sources/SwiftPlot/Interpolator.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
2+
/// An `Interpolator` maps values to a continuum between 0 and 1
3+
public struct Interpolator<Element> {
4+
public var compare: (Element, Element) -> Bool
5+
public var interpolate: (Element, Element, Element) -> Float
6+
7+
public init(
8+
compare areInIncreasingOrder: @escaping (Element, Element) -> Bool,
9+
interpolate: @escaping (Element, Element, Element)->Float
10+
) {
11+
self.compare = areInIncreasingOrder
12+
self.interpolate = interpolate
13+
}
14+
}
15+
16+
extension Interpolator {
17+
18+
public var inverted: Interpolator<Element> {
19+
Interpolator(
20+
compare: { self.compare($0, $1) },
21+
interpolate: { 1 - self.interpolate($0, $1, $2) }
22+
)
23+
}
24+
}
25+
26+
extension Interpolator where Element: Comparable {
27+
public init(interpolate: @escaping (Element, Element, Element)->Float) {
28+
self.init(compare: <, interpolate: interpolate)
29+
}
30+
}
31+
32+
// Linear mapping for numeric types.
33+
34+
extension Interpolator where Element: FloatConvertible {
35+
public static var linear: Interpolator {
36+
Interpolator { value, min, max in
37+
let value = Float(value)
38+
let range = Float(min)...Float(max)
39+
let totalDistance = range.lowerBound.distance(to: range.upperBound)
40+
let valueOffset = range.lowerBound.distance(to: value)
41+
return valueOffset/totalDistance
42+
}
43+
}
44+
}
45+
extension Interpolator where Element: FixedWidthInteger {
46+
public static var linear: Interpolator {
47+
Interpolator { value, min, max in
48+
let distance = min.distance(to: max)
49+
let valDist = min.distance(to: value)
50+
return Float(valDist)/Float(distance)
51+
}
52+
}
53+
}
54+
55+
// Mapping by key-paths.
56+
57+
extension Interpolator {
58+
59+
public static func linearByKeyPath<T>(_ kp: KeyPath<Element, T>) -> Interpolator<Element>
60+
where T: FloatConvertible {
61+
let i = Interpolator<T>.linear
62+
return Interpolator(
63+
compare: { $0[keyPath: kp] < $1[keyPath: kp] },
64+
interpolate: { value, min, max in
65+
return i.interpolate(value[keyPath: kp], min[keyPath: kp], max[keyPath: kp])
66+
})
67+
}
68+
69+
public static func linearByKeyPath<T>(_ kp: KeyPath<Element, T>) -> Interpolator<Element>
70+
where T: FixedWidthInteger {
71+
let i = Interpolator<T>.linear
72+
return Interpolator(
73+
compare: { $0[keyPath: kp] < $1[keyPath: kp] },
74+
interpolate: { value, min, max in
75+
return i.interpolate(value[keyPath: kp], min[keyPath: kp], max[keyPath: kp])
76+
})
77+
}
78+
79+
}

0 commit comments

Comments
 (0)