1
1
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 {
5
16
6
17
public typealias Element = SeriesType . Element . Element
7
18
8
19
public var layout = GraphLayout ( )
20
+
9
21
public var values : SeriesType
10
22
public var interpolator : Interpolator < Element >
11
23
@@ -17,50 +29,13 @@ where SeriesType: Sequence, SeriesType.Element: Sequence,
17
29
}
18
30
}
19
31
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
-
57
32
// Layout and drawing.
58
33
59
34
extension Heatmap : HasGraphLayout , Plot {
60
35
61
36
public struct DrawingData {
62
37
var values : SeriesType ?
63
- var range : ClosedRange < Element > ?
38
+ var range : ( min : Element , max : Element ) ?
64
39
var itemSize = Size . zero
65
40
var rows = 0
66
41
var columns = 0
@@ -70,7 +45,6 @@ extension Heatmap: HasGraphLayout, Plot {
70
45
71
46
var results = DrawingData ( )
72
47
var markers = PlotMarkers ( )
73
-
74
48
// Extract the first (inner) element as a starting point.
75
49
guard let firstElem = values. first ( where: { _ in true } ) ? . first ( where: { _ in true } ) else {
76
50
return ( results, nil )
@@ -83,16 +57,16 @@ extension Heatmap: HasGraphLayout, Plot {
83
57
for row in values {
84
58
var columnsInRow = 0
85
59
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
88
62
columnsInRow += 1
89
63
}
90
64
maxColumns = max ( maxColumns, columnsInRow)
91
65
totalRows += 1
92
66
}
93
67
// Update results.
94
68
results. values = values
95
- results. range = minValue... maxValue
69
+ results. range = ( minValue, maxValue)
96
70
results. rows = totalRows
97
71
results. columns = maxColumns
98
72
results. itemSize = Size (
@@ -116,20 +90,18 @@ extension Heatmap: HasGraphLayout, Plot {
116
90
}
117
91
118
92
public func drawData( _ data: DrawingData , size: Size , renderer: Renderer ) {
119
-
120
-
121
93
guard let values = data. values, let range = data. range else { return }
122
94
123
95
for (rowIdx, row) in values. enumerated ( ) {
124
- for (columnIdx, column ) in row. enumerated ( ) {
96
+ for (columnIdx, element ) in row. enumerated ( ) {
125
97
let rect = Rect (
126
98
origin: Point ( Float ( columnIdx) * data. itemSize. width,
127
99
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 ),
133
105
// location: rect.origin + Point(50,50),
134
106
// textSize: 20,
135
107
// color: .white,
@@ -139,70 +111,82 @@ extension Heatmap: HasGraphLayout, Plot {
139
111
}
140
112
}
141
113
142
- func getColor( of value: Element , range : ClosedRange < Element > ) -> Color {
114
+ private func getColor( of value: Element , min : Element , max : Element ) -> Color {
143
115
let startColor = Color . orange
144
116
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)
148
119
}
149
120
}
150
121
122
+ // Initialisers with default arguments.
151
123
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
+ }
153
132
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 {
156
136
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 )
159
139
}
160
- public func callAsFunction( _ item: Element , in range: ClosedRange < Element > ) -> Float {
161
- interpolate ( item, range)
140
+
141
+ public init ( ) {
142
+ self . init ( interpolator: . linear)
162
143
}
163
144
}
164
145
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)
174
152
}
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)
183
156
}
184
157
}
185
158
186
- extension Interpolator {
159
+ // Collection construction shorthand.
160
+
161
+ extension Sequence where Element: Sequence {
187
162
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)
196
169
}
170
+ }
171
+
172
+ extension RandomAccessCollection {
197
173
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]
205
185
}
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)
206
191
}
207
-
208
192
}
0 commit comments