Skip to content

Commit 9fe90a7

Browse files
committed
Rename Interpolator -> Adapters.Heatmap
Clean up Heatmap's SequencePlot overloads
1 parent 941986e commit 9fe90a7

File tree

5 files changed

+246
-191
lines changed

5 files changed

+246
-191
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
2+
public enum Adapters {}

Sources/SwiftPlot/Color.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ public struct Color {
3232

3333
extension Color {
3434

35-
/// Returns a `Color` whose RBGA components are generated by given `RandomNumberGenerator`.
35+
/// Returns a `Color` whose RBGA components are generated by the given `RandomNumberGenerator`.
3636
///
3737
public static func random<RNG: RandomNumberGenerator>(using generator: inout RNG) -> Color {
3838
return Color(.random(in: 0...1.0, using: &generator),

Sources/SwiftPlot/Heatmap.swift

Lines changed: 178 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,31 @@
33
// - Spacing between blocks
44
// - Setting X/Y axis labels
55
// - Displaying colormap next to plot
6-
// - Collection slicing by filter closure?
76

87
/// A heatmap is a plot of 2-dimensional data, where each value is assigned a colour value along a gradient.
98
///
10-
/// Use the `interpolator` property to control how values are graded. For example, if your data structure has
11-
/// a salient integer or floating-point property, `Interpolator.linearByKeyPath` will allow you to grade values by that property.
9+
/// Use the `adapter` property to control how values are graded. For example, if your data structure has
10+
/// a salient integer or floating-point property, `.keyPath` will allow you to grade values by that property:
11+
///
12+
/// ```swift
13+
/// let data: [[MyObject]] = ...
14+
/// data.plots.heatmap(adapter: .keyPath(\.importantProperty)) {
15+
/// $0.colorMap = .fiveColorHeatmap
16+
/// }
17+
/// ```
1218
public struct Heatmap<SeriesType> where SeriesType: Sequence, SeriesType.Element: Sequence {
1319

1420
public typealias Element = SeriesType.Element.Element
1521

1622
public var layout = GraphLayout()
1723

1824
public var values: SeriesType
19-
public var interpolator: Interpolator<Element>
25+
public var adapter: Adapters.Heatmap<Element>
2026
public var colorMap: ColorMap = .fiveColorHeatMap
2127

22-
public init(values: SeriesType, interpolator: Interpolator<Element>) {
28+
public init(values: SeriesType, interpolator: Adapters.Heatmap<Element>) {
2329
self.values = values
24-
self.interpolator = interpolator
30+
self.adapter = interpolator
2531
self.layout.drawsGridOverForeground = true
2632
self.layout.markerLabelAlignment = .betweenMarkers
2733
self.showGrid = false
@@ -68,8 +74,8 @@ extension Heatmap: HasGraphLayout, Plot {
6874
for row in values {
6975
var columnsInRow = 0
7076
for column in row {
71-
maxValue = interpolator.compare(maxValue, column) ? column : maxValue
72-
minValue = interpolator.compare(minValue, column) ? minValue : column
77+
maxValue = adapter.compare(maxValue, column) ? column : maxValue
78+
minValue = adapter.compare(minValue, column) ? minValue : column
7379
columnsInRow += 1
7480
}
7581
maxColumns = max(maxColumns, columnsInRow)
@@ -121,7 +127,7 @@ extension Heatmap: HasGraphLayout, Plot {
121127
Float(rowIdx) * data.itemSize.height),
122128
size: data.itemSize
123129
)
124-
let offset = interpolator.interpolate(element, range.min, range.max)
130+
let offset = adapter.interpolate(element, range.min, range.max)
125131
let color = colorMap.colorForOffset(offset)
126132
renderer.drawSolidRect(rect, fillColor: color, hatchPattern: .none)
127133
// renderer.drawText(text: String(describing: element),
@@ -135,139 +141,188 @@ extension Heatmap: HasGraphLayout, Plot {
135141
}
136142
}
137143

138-
// Initialisers with default arguments.
144+
// MARK: - SequencePlots API.
139145

140-
extension Heatmap
141-
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
142-
SeriesType.ArrayLiteralElement == SeriesType.Element {
143-
144-
public init(interpolator: Interpolator<Element>) {
145-
self.init(values: [[]], interpolator: interpolator)
146-
}
146+
// 2D Datasets.
147+
148+
extension SequencePlots where Base.Element: Sequence {
149+
150+
/// Returns a heatmap of values from this 2-dimensional sequence.
151+
///
152+
/// - parameters:
153+
/// - adapter: A function or `KeyPath` which maps values to a continuum between 0 and 1.
154+
/// - style: A closure which applies a style to the heatmap.
155+
/// - returns: A heatmap plot of the sequence's inner items.
156+
///
157+
public func heatmap(
158+
adapter: Adapters.Heatmap<Base.Element.Element>,
159+
style: (inout Heatmap<Base>)->Void = { _ in }
160+
) -> Heatmap<Base> {
161+
var graph = Heatmap(values: base, interpolator: adapter)
162+
style(&graph)
163+
return graph
164+
}
147165
}
148166

149-
extension Heatmap
150-
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
151-
SeriesType.ArrayLiteralElement == SeriesType.Element, Element: FloatConvertible {
152-
153-
public init(values: SeriesType) {
154-
self.init(values: values, interpolator: .linear)
155-
}
156-
157-
public init() {
158-
self.init(interpolator: .linear)
159-
}
167+
extension SequencePlots where Base.Element: Sequence,
168+
Base.Element.Element: Strideable, Base.Element.Element.Stride: BinaryFloatingPoint {
169+
170+
/// Returns a heatmap of values from this 2-dimensional sequence.
171+
///
172+
public func heatmap(
173+
style: (inout Heatmap<Base>)->Void = { _ in }
174+
) -> Heatmap<Base> {
175+
return heatmap(adapter: .linear, style: style)
176+
}
160177
}
161178

162-
extension Heatmap
163-
where SeriesType: ExpressibleByArrayLiteral, SeriesType.Element: ExpressibleByArrayLiteral,
164-
SeriesType.ArrayLiteralElement == SeriesType.Element, Element: FixedWidthInteger {
165-
166-
public init(values: SeriesType) {
167-
self.init(values: values, interpolator: .linear)
168-
}
169-
170-
public init() {
171-
self.init(interpolator: .linear)
172-
}
179+
extension SequencePlots where Base.Element: Sequence,
180+
Base.Element.Element: FixedWidthInteger {
181+
182+
/// Returns a heatmap of values from this 2-dimensional sequence.
183+
///
184+
public func heatmap(
185+
style: (inout Heatmap<Base>)->Void = { _ in }
186+
) -> Heatmap<Base> {
187+
return heatmap(adapter: .linear, style: style)
188+
}
173189
}
174190

175-
// Collection construction shorthand.
191+
// 1D Datasets.
176192

177-
extension SequencePlots where Base.Element: Sequence {
178-
179-
/// Returns a heatmap of values from this 2-dimensional sequence.
180-
/// - parameters:
181-
/// - interpolator: A function or `KeyPath` which maps values to a continuum between 0 and 1.
182-
/// - returns: A heatmap plot of the sequence's inner items.
183-
public func heatmap(
184-
interpolator: Interpolator<Base.Element.Element>,
185-
style: (inout Heatmap<Base>)->Void = { _ in }
186-
) -> Heatmap<Base> {
193+
extension SequencePlots where Base: Collection {
187194

188-
var graph = Heatmap(values: base, interpolator: interpolator)
189-
style(&graph)
190-
return graph
191-
}
195+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
196+
///
197+
/// - parameters:
198+
/// - width: The width of the heatmap to generate. Must be greater than 0.
199+
/// - adapter: A function or `KeyPath` which maps values to a continuum between 0 and 1.
200+
/// - returns: A heatmap plot of the collection's values.
201+
/// - complexity: O(n). Consider though, that rendering a heatmap or copying to a `RamdomAccessCollection`
202+
/// is also at least O(n), and this does not copy the data.
203+
///
204+
public func heatmap(
205+
width: Int,
206+
adapter: Adapters.Heatmap<Base.Element>,
207+
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
208+
) -> Heatmap<[Base.SubSequence]> {
209+
210+
precondition(width > 0, "Cannot build a heatmap with zero or negative width")
211+
var rows = [Base.SubSequence]()
212+
var rowStart = base.startIndex
213+
while rowStart != base.endIndex {
214+
guard let rowEnd = base.index(rowStart, offsetBy: width, limitedBy: base.endIndex) else {
215+
rows.append(base[rowStart..<base.endIndex])
216+
break
217+
}
218+
rows.append(base[rowStart..<rowEnd])
219+
rowStart = rowEnd
220+
}
221+
return rows.plots.heatmap(adapter: adapter, style: style)
222+
}
192223
}
193224

194-
extension SequencePlots where Base: Collection {
195-
196-
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
197-
/// - parameters:
198-
/// - width: The width of the heatmap to generate. Must be greater than 0.
199-
/// - interpolator: A function or `KeyPath` which maps values to a continuum between 0 and 1.
200-
/// - returns: A heatmap plot of the collection's values.
201-
/// - complexity: O(n). Consider though, that rendering a heatmap or copying to a `RamdomAccessCollection`
202-
/// is also at least O(n), and this does not copy the data.
203-
public func heatmap(
204-
width: Int,
205-
interpolator: Interpolator<Base.Element>,
206-
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
207-
) -> Heatmap<[Base.SubSequence]> {
225+
extension SequencePlots where Base: Collection,
226+
Base.Element: Strideable, Base.Element.Stride: BinaryFloatingPoint {
208227

209-
precondition(width > 0, "Cannot build a histogram with zero or negative width")
210-
var rows = [Base.SubSequence]()
211-
var rowStart = base.startIndex
212-
while rowStart != base.endIndex {
213-
guard let rowEnd = base.index(rowStart, offsetBy: width, limitedBy: base.endIndex) else {
214-
rows.append(base[rowStart..<base.endIndex])
215-
break
216-
}
217-
rows.append(base[rowStart..<rowEnd])
218-
rowStart = rowEnd
228+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
229+
///
230+
/// - parameters:
231+
/// - width: The width of the heatmap to generate. Must be greater than 0.
232+
/// - returns: A heatmap plot of the collection's values.
233+
/// - complexity: O(n). Consider though, that rendering a heatmap or copying to a `RamdomAccessCollection`
234+
/// is also at least O(n), and this does not copy the data.
235+
///
236+
public func heatmap(
237+
width: Int,
238+
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
239+
) -> Heatmap<[Base.SubSequence]> {
240+
return heatmap(width: width, adapter: .linear, style: style)
219241
}
220-
return rows.plots.heatmap(interpolator: interpolator, style: style)
221-
}
222242
}
223243

224-
extension SequencePlots where Base: RandomAccessCollection {
225-
226-
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
227-
/// - parameters:
228-
/// - width: The width of the heatmap to generate. Must be greater than 0.
229-
/// - interpolator: A function or `KeyPath` which maps values to a continuum between 0 and 1.
230-
/// - returns: A heatmap plot of the collection's values.
231-
public func heatmap(
232-
width: Int,
233-
interpolator: Interpolator<Base.Element>,
234-
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
235-
) -> Heatmap<[Base.SubSequence]> {
244+
extension SequencePlots where Base: Collection,
245+
Base.Element: FixedWidthInteger {
236246

237-
precondition(width > 0, "Cannot build a histogram with zero or negative width")
238-
let height = Int((Float(base.count) / Float(width)).rounded(.up))
239-
return (0..<height)
240-
.map { base._sliceForRow($0, width: width) }
241-
.plots.heatmap(interpolator: interpolator, style: style)
242-
}
243-
244-
/// Returns a heatmap of this collection's values, generated by the data in to `height` rows.
245-
/// - parameters:
246-
/// - height: The height of the heatmap to generate. Must be greater than 0.
247-
/// - interpolator: A function or `KeyPath` which maps values to a continuum between 0 and 1.
248-
/// - returns: A heatmap plot of the collection's values.
249-
public func heatmap(
250-
height: Int,
251-
interpolator: Interpolator<Base.Element>,
252-
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
253-
) -> Heatmap<[Base.SubSequence]> {
247+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
248+
///
249+
/// - parameters:
250+
/// - width: The width of the heatmap to generate. Must be greater than 0.
251+
/// - returns: A heatmap plot of the collection's values.
252+
/// - complexity: O(n). Consider though, that rendering a heatmap or copying to a `RamdomAccessCollection`
253+
/// is also at least O(n), and this does not copy the data.
254+
///
255+
public func heatmap(
256+
width: Int,
257+
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
258+
) -> Heatmap<[Base.SubSequence]> {
259+
return heatmap(width: width, adapter: .linear, style: style)
260+
}
261+
}
262+
263+
extension SequencePlots where Base: RandomAccessCollection {
254264

255-
precondition(height > 0, "Cannot build a histogram with zero or negative height")
256-
let width = Int((Float(base.count) / Float(height)).rounded(.up))
257-
return (0..<height)
258-
.map { base._sliceForRow($0, width: width) }
259-
.plots.heatmap(interpolator: interpolator, style: style)
260-
}
265+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
266+
///
267+
/// - parameters:
268+
/// - width: The width of the heatmap to generate. Must be greater than 0.
269+
/// - adapter: A function or `KeyPath` which maps values to a continuum between 0 and 1.
270+
/// - returns: A heatmap plot of the collection's values.
271+
///
272+
public func heatmap(
273+
width: Int,
274+
adapter: Adapters.Heatmap<Base.Element>,
275+
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
276+
) -> Heatmap<[Base.SubSequence]> {
277+
278+
func sliceForRow(_ row: Int, width: Int) -> Base.SubSequence {
279+
guard let start = base.index(base.startIndex, offsetBy: row * width, limitedBy: base.endIndex) else {
280+
return base[base.startIndex..<base.startIndex]
281+
}
282+
guard let end = base.index(start, offsetBy: width, limitedBy: base.endIndex) else {
283+
return base[start..<base.endIndex]
284+
}
285+
return base[start..<end]
286+
}
287+
288+
precondition(width > 0, "Cannot build a histogram with zero or negative width")
289+
let height = Int((Float(base.count) / Float(width)).rounded(.up))
290+
return (0..<height)
291+
.map { sliceForRow($0, width: width) }
292+
.plots.heatmap(adapter: adapter, style: style)
293+
}
261294
}
262295

263-
extension RandomAccessCollection {
264-
fileprivate func _sliceForRow(_ row: Int, width: Int) -> SubSequence {
265-
guard let start = index(startIndex, offsetBy: row * width, limitedBy: endIndex) else {
266-
return self[startIndex..<startIndex]
296+
extension SequencePlots where Base: RandomAccessCollection,
297+
Base.Element: Strideable, Base.Element.Stride: BinaryFloatingPoint {
298+
299+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
300+
///
301+
/// - parameters:
302+
/// - width: The width of the heatmap to generate. Must be greater than 0.
303+
/// - returns: A heatmap plot of the collection's values.
304+
///
305+
public func heatmap(
306+
width: Int,
307+
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
308+
) -> Heatmap<[Base.SubSequence]> {
309+
return heatmap(width: width, adapter: .linear, style: style)
267310
}
268-
guard let end = index(start, offsetBy: width, limitedBy: endIndex) else {
269-
return self[start..<endIndex]
311+
}
312+
313+
extension SequencePlots where Base: RandomAccessCollection,
314+
Base.Element: FixedWidthInteger {
315+
316+
/// Returns a heatmap of this collection's values, generated by slicing rows with the given width.
317+
///
318+
/// - parameters:
319+
/// - width: The width of the heatmap to generate. Must be greater than 0.
320+
/// - returns: A heatmap plot of the collection's values.
321+
///
322+
public func heatmap(
323+
width: Int,
324+
style: (inout Heatmap<[Base.SubSequence]>)->Void = { _ in }
325+
) -> Heatmap<[Base.SubSequence]> {
326+
return heatmap(width: width, adapter: .linear, style: style)
270327
}
271-
return self[start..<end]
272-
}
273328
}

0 commit comments

Comments
 (0)