-
Notifications
You must be signed in to change notification settings - Fork 12
Expand file tree
/
Copy pathcontour.typ
More file actions
370 lines (337 loc) · 11.2 KB
/
Copy pathcontour.typ
File metadata and controls
370 lines (337 loc) · 11.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
#import "/src/cetz.typ": draw
#import "util.typ"
#import "sample.typ"
// Find contours of a 2D array by using marching squares algorithm
//
// - data (array): A 2D array of floats where the first index is the row and the second index is the column
// - offset (float): Z value threshold of a cell compare with `op` to, to count as true
// - op (auto,string,function): Z value comparison oparator:
// / `">", ">=", "<", "<=", "!=", "=="`: Use the passed operator to compare z.
// / `auto`: Use ">=" for positive z values, "<=" for negative z values.
// / `<function>`: If set to a function, that function gets called
// with two arguments, the z value `z1` to compare against and
// the z value `z2` of the data and must return a boolean: `(z1, z2) => boolean`.
// - interpolate (bool): Enable cell interpolation for smoother lines
// - connect-domain (bool): Treat the sample domain boundary as an exterior edge.
// This closes regions against the plot boundary, which is useful for fills but
// not for contour lines.
// - contour-limit (int): Contour limit after which the algorithm panics
// -> array: Array of contour point arrays
#let find-contours(data,
offset,
op: auto,
interpolate: true,
connect-domain: false,
contour-limit: 50) = {
assert(data != none and type(data) == array,
message: "Data must be of type array")
assert(type(offset) in (int, float),
message: "Offset must be numeric")
let n-rows = data.len()
let n-cols = data.at(0).len()
if n-rows < 2 or n-cols < 2 {
return ()
}
assert(op == auto or type(op) in (str, function),
message: "Operator must be of type auto, string or function")
if op == auto {
op = if offset < 0 { "<=" } else { ">=" }
}
if type(op) == str {
assert(op in ("<", "<=", ">", ">=", "==", "!="),
message: "Operator must be one of: <, <=, >, >=, != or ==")
}
// Return if data is set
let is-set = if type(op) == function {
v => op(offset, v)
} else if op == "==" {
v => v == offset
} else if op == "!=" {
v => v != offset
} else if op == "<" {
v => v < offset
} else if op == "<=" {
v => v <= offset
} else if op == ">" {
v => v > offset
} else if op == ">=" {
v => v >= offset
}
// Build a binary map that has 0 for unset and 1 for set cells
let bin-data = data.map(r => r.map(is-set))
// Get case (0 to 15)
let get-case(tl, tr, bl, br) = {
int(tl) * 8 + int(tr) * 4 + int(br) * 2 + int(bl)
}
let lerp(a, b) = {
if a == b { return a }
else if a == none { return 1 }
else if b == none { return 0 }
return (offset - a) / (b - a)
}
let segments = ()
let x-range = if connect-domain { range(-1, n-cols) } else { range(0, n-cols - 1) }
let y-range = if connect-domain { range(-1, n-rows) } else { range(0, n-rows - 1) }
// Get binary data at x, y
let get-bin(x, y) = {
if x >= 0 and x < n-cols and y >= 0 and y < n-rows {
return bin-data.at(y).at(x)
}
return false
}
// Get data point for x, y coordinate
let get-data(x, y) = {
if x >= 0 and x < n-cols and y >= 0 and y < n-rows {
return float(data.at(y).at(x))
}
return none
}
for y in y-range {
for x in x-range {
let tl = get-bin(x, y)
let tr = get-bin(x + 1, y)
let bl = get-bin(x, y + 1)
let br = get-bin(x + 1, y + 1)
// Corner data
//
// nw-----ne
// | |
// | |
// | |
// sw-----se
let nw = get-data(x, y)
let ne = get-data(x + 1, y)
let se = get-data(x + 1, y + 1)
let sw = get-data(x, y + 1)
// Interpolated edge points
//
// +-- a --+
// | |
// d b
// | |
// +-- c --+
let a = (x + .5, y)
let b = (x + 1, y + .5)
let c = (x + .5, y + 1)
let d = (x, y + .5)
if interpolate {
a = (x + lerp(nw, ne), y)
b = (x + 1, y + lerp(ne, se))
c = (x + lerp(sw, se), y + 1)
d = (x, y + lerp(nw, sw))
}
let case = get-case(tl, tr, bl, br)
if case in (1, 14) {
segments.push((d, c))
} else if case in (2, 13) {
segments.push((b, c))
} else if case in (3, 12) {
segments.push((d, b))
} else if case in (4, 11) {
segments.push((a, b))
} else if case == 5 {
segments.push((d, a))
segments.push((c, b))
} else if case in (6, 9) {
segments.push((c, a))
} else if case in (7, 8) {
segments.push((d, a))
} else if case == 10 {
segments.push((a, b))
segments.push((c, d))
}
}
}
// Join lines to one or more contours
// This is done by searching for the next line
// that starts at the current contours head or tail
// point. If found, push the other coordinate to
// the contour. If no line could be found, push a
// new contour.
let contours = ()
while segments.len() > 0 {
if contours.len() == 0 {
contours.push(segments.remove(0))
}
let found = false
let i = 0
while i < segments.len() {
let (a, b) = segments.at(i)
let (h, t) = (contours.last().first(),
contours.last().last())
if a == t {
contours.last().push(b)
segments.remove(i)
found = true
} else if b == t {
contours.last().push(a)
segments.remove(i)
found = true
} else if a == h {
contours.last().insert(0, b)
segments.remove(i)
found = true
} else if b == h {
contours.last().insert(0, a)
segments.remove(i)
found = true
} else {
i += 1
}
}
// Insert the next contour
if not found {
contours.push(segments.remove(0))
}
// Check limit
assert(contours.len() <= contour-limit,
message: "Countour limit reached! Raise contour-limit if you " +
"think this is not an error")
}
return contours
}
// Convert contour data points from sample-space to plot coordinates.
#let _scale-contours(contours, x-min, y-min, dx, dy, z) = {
contours.map(contour => (
z: z,
line-data: contour.map(pt => (
pt.at(0) * dx + x-min,
pt.at(1) * dy + y-min,
)),
))
}
// Prepare line data
#let _prepare(self, ctx) = {
let (x, y) = (ctx.x, ctx.y)
self.stroke-contours = self.stroke-contours.map(c => {
c.stroke-paths = util.compute-stroke-paths(c.line-data, x, y)
return c
})
if self.fill {
self.fill-contours = self.fill-contours.map(c => {
c.fill-paths = util.compute-fill-paths(c.line-data, x, y)
return c
})
}
return self
}
// Stroke line data
#let _stroke(self, ctx) = {
for c in self.stroke-contours {
for p in c.stroke-paths {
draw.line(..p, fill: none, close: p.first() == p.last())
}
}
}
// Fill line data
#let _fill(self, ctx) = {
if not self.fill { return }
for c in self.fill-contours {
for p in c.fill-paths {
draw.line(..p, stroke: none, close: p.first() == p.last())
}
}
}
/// Add a contour plot of a sampled function or a matrix.
///
/// ```cexample
/// plot.plot(size: (2,2), x-tick-step: none, y-tick-step: none, {
/// plot.add-contour(x-domain: (-3, 3), y-domain: (-3, 3),
/// style: (fill: rgb(50,50,250,50)),
/// fill: true,
/// op: "<", // Find contours where data < z
/// z: (2.5, 2, 1), // Z values to find contours for
/// (x, y) => calc.sqrt(x * x + y * y))
/// })
/// ```
///
/// - data (array, function): A function of the signature `(x, y) => z`
/// or an array of arrays of floats (a matrix) where the first
/// index is the row and the second index is the column.
/// - z (float, array): Z values to plot. Contours containing values
/// above z (z >= 0) or below z (z < 0) get plotted.
/// If you specify multiple z values, they get plotted in the order of specification.
/// - x-domain (domain): X axis domain used if `data` is a function, that is the
/// domain inside the function gets sampled.
/// - y-domain (domain): Y axis domain used if `data` is a function, see `x-domain`.
/// - x-samples (int): X axis domain samples (2 < n). Note that contour finding
/// can be quite slow. Using a big sample count can improve accuracy but can
/// also lead to bad compilation performance.
/// - y-samples (int): Y axis domain samples (2 < n)
/// - interpolate (bool): Use linear interpolation between sample values which can
/// improve the resulting plot, especially if the contours are curved.
/// - op (auto,string,function): Z value comparison oparator:
/// / `">", ">=", "<", "<=", "!=", "=="`: Use the operator for comparison of `z` to
/// the values from `data`.
/// / `auto`: Use ">=" for positive z values, "<=" for negative z values.
/// / `<function>`: Call comparison function of the format `(plot-z, data-z) => boolean`,
/// where `plot-z` is the z-value from the plots `z` argument and `data-z`
/// is the z-value of the data getting plotted. The function must return true
/// if at the combinations of arguments a contour is detected.
/// - fill (bool): Fill each contour
/// - style (style): Style to use for plotting, can be used with a palette function. Note
/// that all z-levels use the same style!
/// - axes (axes): Name of the axes to use for plotting.
/// - limit (int): Limit of contours to create per z value before the function panics
/// - label (none,content): Plot legend label to show. The legend preview for
/// contour plots is a little rectangle drawn with the contours style.
#let add-contour(data,
label: none,
z: (1,),
x-domain: (0, 1),
y-domain: (0, 1),
x-samples: 25,
y-samples: 25,
interpolate: true,
op: auto,
axes: ("x", "y"),
style: (:),
fill: false,
limit: 50,
) = {
// Sample a x/y function
if type(data) == function {
data = sample.sample-fn2(data,
x-domain, y-domain,
x-samples, y-samples)
}
// Find matrix dimensions
assert(type(data) == array)
let (x-min, x-max) = x-domain
let dx = (x-max - x-min) / (data.at(0).len() - 1)
let (y-min, y-max) = y-domain
let dy = (y-max - y-min) / (data.len() - 1)
let stroke-contours = ()
let fill-contours = ()
let z = if type(z) == array { z } else { (z,) }
for z in z {
stroke-contours += _scale-contours(
find-contours(data, z, op: op, interpolate: interpolate, contour-limit: limit),
x-min, y-min, dx, dy, z)
if fill {
fill-contours += _scale-contours(
find-contours(data, z, op: op, interpolate: interpolate, connect-domain: true, contour-limit: limit),
x-min, y-min, dx, dy, z)
}
}
return ((
type: "contour",
label: label,
stroke-contours: stroke-contours,
fill-contours: fill-contours,
axes: axes,
x-domain: x-domain,
y-domain: y-domain,
style: style,
fill: fill,
mark: none,
mark-style: none,
plot-prepare: _prepare,
plot-stroke: _stroke,
plot-fill: _fill,
plot-legend-preview: self => {
if not self.fill { self.style.fill = none }
draw.rect((0,0), (1,1), ..self.style)
}
),)
}