@data-profile/fingerprint 0.0.0-stage → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/LICENSE +21 -0
- package/README.md +345 -2
- package/dist/chunk-y8jc1j2x.js +447 -0
- package/dist/chunk-y8jc1j2x.js.map +15 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +10 -0
- package/dist/react/index.d.ts +300 -0
- package/dist/react/index.js +643 -0
- package/dist/react/index.js.map +14 -0
- package/dist/styles.css +117 -0
- package/package.json +75 -4
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
// temp/src/form.ts
|
|
2
|
+
import { unitHash } from "@data-profile/core";
|
|
3
|
+
var clamp = (value, low, high) => Math.min(high, Math.max(low, value));
|
|
4
|
+
var quantise = (value, step = 0.2) => Math.round(value / step) * step;
|
|
5
|
+
function formFromSeed(seed) {
|
|
6
|
+
const h = (key) => unitHash(`${seed}:${key}`);
|
|
7
|
+
return {
|
|
8
|
+
pattern: "loop",
|
|
9
|
+
hand: h("hand") < 0.5 ? -1 : 1,
|
|
10
|
+
coreX: quantise(h("x") * 2 - 1) * 0.5,
|
|
11
|
+
coreY: quantise(h("y") * 2 - 1) * 0.5,
|
|
12
|
+
spread: quantise(h("spread")),
|
|
13
|
+
tilt: Math.round((h("tilt") - 0.5) * 12)
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function resolveForm(seed, shape = {}) {
|
|
17
|
+
const resolved = { ...formFromSeed(seed), ...shape };
|
|
18
|
+
return {
|
|
19
|
+
...resolved,
|
|
20
|
+
coreX: clamp(resolved.coreX, -1, 1),
|
|
21
|
+
coreY: clamp(resolved.coreY, -1, 1),
|
|
22
|
+
spread: clamp(resolved.spread, 0, 1),
|
|
23
|
+
tilt: clamp(resolved.tilt, -10, 10)
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function lean(items, dimension) {
|
|
27
|
+
if (!dimension || dimension.categories.length < 2)
|
|
28
|
+
return 0.5;
|
|
29
|
+
const index = new Map(dimension.categories.map((c, i) => [c.id, i / (dimension.categories.length - 1)]));
|
|
30
|
+
let sum = 0;
|
|
31
|
+
let n = 0;
|
|
32
|
+
for (const item of items) {
|
|
33
|
+
const at = index.get(item.values[dimension.id] ?? "");
|
|
34
|
+
if (at !== undefined) {
|
|
35
|
+
sum += at;
|
|
36
|
+
n += 1;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return n ? sum / n : 0.5;
|
|
40
|
+
}
|
|
41
|
+
function formFromItems(items, dimensions, seed = "fingerprint") {
|
|
42
|
+
const [first, second, third] = dimensions;
|
|
43
|
+
let top = 0;
|
|
44
|
+
if (first) {
|
|
45
|
+
const counts = new Map;
|
|
46
|
+
for (const item of items) {
|
|
47
|
+
const value = item.values[first.id];
|
|
48
|
+
if (value != null)
|
|
49
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
50
|
+
}
|
|
51
|
+
top = items.length ? Math.max(0, ...counts.values()) / items.length : 0;
|
|
52
|
+
}
|
|
53
|
+
const total = dimensions.reduce((sum, d) => sum + d.categories.length, 0);
|
|
54
|
+
const used = dimensions.reduce((sum, d) => sum + d.categories.filter((c) => items.some((item) => item.values[d.id] === c.id)).length, 0);
|
|
55
|
+
const across = lean(items, second);
|
|
56
|
+
return resolveForm(seed, {
|
|
57
|
+
pattern: items.length < 12 ? "arch" : top >= 0.4 ? "loop" : "whorl",
|
|
58
|
+
hand: across < 0.5 ? -1 : 1,
|
|
59
|
+
coreX: quantise((across - 0.5) * 2),
|
|
60
|
+
coreY: quantise((lean(items, third) - 0.5) * 2),
|
|
61
|
+
spread: total ? quantise(used / total) : 0.5
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// temp/src/layout.ts
|
|
66
|
+
import { encodeProfile } from "@data-profile/core";
|
|
67
|
+
|
|
68
|
+
// temp/src/ridges.ts
|
|
69
|
+
var FRAME = { width: 400, height: 520, cx: 200, cy: 260, rx: 180, ry: 240 };
|
|
70
|
+
var DEG = Math.PI / 180;
|
|
71
|
+
var outlineRadius = (x, y) => Math.hypot((x - FRAME.cx) / FRAME.rx, (y - FRAME.cy) / FRAME.ry);
|
|
72
|
+
var smoothstep = (a, b, x) => {
|
|
73
|
+
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
|
|
74
|
+
return t * t * (3 - 2 * t);
|
|
75
|
+
};
|
|
76
|
+
function singularities(shape) {
|
|
77
|
+
const { rx, ry, cx, cy } = FRAME;
|
|
78
|
+
const centre = [cx + shape.coreX * 0.12 * rx, cy - 0.1 * ry + shape.coreY * 0.1 * ry];
|
|
79
|
+
const [x, y] = centre;
|
|
80
|
+
if (shape.pattern === "arch")
|
|
81
|
+
return { centre, cores: [], deltas: [] };
|
|
82
|
+
if (shape.pattern === "loop")
|
|
83
|
+
return {
|
|
84
|
+
centre,
|
|
85
|
+
cores: [centre],
|
|
86
|
+
deltas: [
|
|
87
|
+
[x + shape.hand * (0.4 + 0.25 * shape.spread) * rx, y + (0.38 + 0.12 * shape.spread) * ry]
|
|
88
|
+
]
|
|
89
|
+
};
|
|
90
|
+
const twist = 0.06 * rx;
|
|
91
|
+
return {
|
|
92
|
+
centre,
|
|
93
|
+
cores: [
|
|
94
|
+
[x - twist, y - shape.hand * 0.025 * ry],
|
|
95
|
+
[x + twist, y + shape.hand * 0.025 * ry]
|
|
96
|
+
],
|
|
97
|
+
deltas: [
|
|
98
|
+
[x - (0.5 + 0.2 * shape.spread) * rx, y + 0.42 * ry],
|
|
99
|
+
[x + (0.5 + 0.2 * shape.spread) * rx, y + 0.42 * ry]
|
|
100
|
+
]
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
function orientation(x, y, shape, s) {
|
|
104
|
+
const tilt = shape.tilt * DEG;
|
|
105
|
+
let theta;
|
|
106
|
+
if (shape.pattern === "arch") {
|
|
107
|
+
const width = (0.35 + 0.25 * shape.spread) * FRAME.rx;
|
|
108
|
+
const u = (x - s.centre[0]) / width;
|
|
109
|
+
const height = 0.3 * FRAME.ry * Math.max(0, 1 - Math.abs(y - s.centre[1]) / FRAME.ry);
|
|
110
|
+
theta = tilt + Math.atan(2 * height * u / width * Math.exp(-u * u));
|
|
111
|
+
} else {
|
|
112
|
+
let sum = 0;
|
|
113
|
+
for (const [px, py] of s.cores)
|
|
114
|
+
sum += Math.atan2(y - py, x - px);
|
|
115
|
+
for (const [px, py] of s.deltas)
|
|
116
|
+
sum -= Math.atan2(y - py, x - px);
|
|
117
|
+
theta = tilt + sum / 2;
|
|
118
|
+
}
|
|
119
|
+
const r = outlineRadius(x, y);
|
|
120
|
+
const over = smoothstep(FRAME.cy + 0.25 * FRAME.ry, FRAME.cy - 0.25 * FRAME.ry, y);
|
|
121
|
+
const weight = smoothstep(0.68, 1, r) * (0.4 + 0.6 * over);
|
|
122
|
+
if (weight === 0)
|
|
123
|
+
return theta;
|
|
124
|
+
const tangent = Math.atan2((x - FRAME.cx) / FRAME.rx ** 2, -(y - FRAME.cy) / FRAME.ry ** 2);
|
|
125
|
+
const vx = (1 - weight) * Math.cos(2 * theta) + weight * Math.cos(2 * tangent);
|
|
126
|
+
const vy = (1 - weight) * Math.sin(2 * theta) + weight * Math.sin(2 * tangent);
|
|
127
|
+
return Math.atan2(vy, vx) / 2;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class PointGrid {
|
|
131
|
+
size;
|
|
132
|
+
cells = new Map;
|
|
133
|
+
constructor(size) {
|
|
134
|
+
this.size = size;
|
|
135
|
+
}
|
|
136
|
+
key = (gx, gy) => gx * 4096 + gy;
|
|
137
|
+
add(x, y, line, index) {
|
|
138
|
+
const k = this.key(Math.floor(x / this.size), Math.floor(y / this.size));
|
|
139
|
+
const cell = this.cells.get(k);
|
|
140
|
+
if (cell)
|
|
141
|
+
cell.push({ x, y, line, index });
|
|
142
|
+
else
|
|
143
|
+
this.cells.set(k, [{ x, y, line, index }]);
|
|
144
|
+
return k;
|
|
145
|
+
}
|
|
146
|
+
removeLine(line, keys) {
|
|
147
|
+
for (const k of new Set(keys)) {
|
|
148
|
+
const kept = (this.cells.get(k) ?? []).filter((p) => p.line !== line);
|
|
149
|
+
if (kept.length)
|
|
150
|
+
this.cells.set(k, kept);
|
|
151
|
+
else
|
|
152
|
+
this.cells.delete(k);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
near(x, y, distance, counts) {
|
|
156
|
+
const gx = Math.floor(x / this.size);
|
|
157
|
+
const gy = Math.floor(y / this.size);
|
|
158
|
+
for (let dx = -1;dx <= 1; dx++)
|
|
159
|
+
for (let dy = -1;dy <= 1; dy++)
|
|
160
|
+
for (const p of this.cells.get(this.key(gx + dx, gy + dy)) ?? [])
|
|
161
|
+
if ((p.x - x) ** 2 + (p.y - y) ** 2 < distance ** 2 && counts(p))
|
|
162
|
+
return true;
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
var OUTLINE_LIMIT = 0.985;
|
|
167
|
+
function ridges(shape, spacing) {
|
|
168
|
+
const s = singularities(shape);
|
|
169
|
+
const step = spacing * 0.3;
|
|
170
|
+
const selfGap = Math.ceil(3 * spacing / step);
|
|
171
|
+
const minLength = Math.ceil(2 * spacing / step);
|
|
172
|
+
const maxSteps = Math.ceil(4 * Math.PI * (FRAME.rx + FRAME.ry) / step);
|
|
173
|
+
const grid = new PointGrid(spacing);
|
|
174
|
+
const lines = [];
|
|
175
|
+
const singular = [...s.cores, ...s.deltas];
|
|
176
|
+
const free = (x, y, line, index) => outlineRadius(x, y) <= OUTLINE_LIMIT && singular.every(([px, py]) => Math.hypot(x - px, y - py) > spacing * 0.6) && !grid.near(x, y, spacing, (p) => p.line !== line || Math.abs(p.index - index) > selfGap);
|
|
177
|
+
const direction = (x, y, previous) => {
|
|
178
|
+
const theta = orientation(x, y, shape, s);
|
|
179
|
+
const v = [Math.cos(theta), Math.sin(theta)];
|
|
180
|
+
return v[0] * previous[0] + v[1] * previous[1] < 0 ? [-v[0], -v[1]] : v;
|
|
181
|
+
};
|
|
182
|
+
const trace = (seed) => {
|
|
183
|
+
const line = lines.length;
|
|
184
|
+
const theta = orientation(seed[0], seed[1], shape, s);
|
|
185
|
+
const keys = [grid.add(seed[0], seed[1], line, 0)];
|
|
186
|
+
const halves = [1, -1].map((sign) => {
|
|
187
|
+
const points2 = [];
|
|
188
|
+
let p = seed;
|
|
189
|
+
let previous = [sign * Math.cos(theta), sign * Math.sin(theta)];
|
|
190
|
+
for (let i = 1;i <= maxSteps; i++) {
|
|
191
|
+
const v1 = direction(p[0], p[1], previous);
|
|
192
|
+
const v2 = direction(p[0] + v1[0] * step / 2, p[1] + v1[1] * step / 2, v1);
|
|
193
|
+
if (v2[0] * previous[0] + v2[1] * previous[1] < 0.8)
|
|
194
|
+
break;
|
|
195
|
+
const q = [p[0] + v2[0] * step, p[1] + v2[1] * step];
|
|
196
|
+
if (!free(q[0], q[1], line, sign * i))
|
|
197
|
+
break;
|
|
198
|
+
keys.push(grid.add(q[0], q[1], line, sign * i));
|
|
199
|
+
points2.push(q);
|
|
200
|
+
p = q;
|
|
201
|
+
previous = v2;
|
|
202
|
+
}
|
|
203
|
+
return points2;
|
|
204
|
+
});
|
|
205
|
+
const points = [...halves[1].reverse(), seed, ...halves[0]];
|
|
206
|
+
if (points.length < minLength) {
|
|
207
|
+
grid.removeLine(line, keys);
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
return points;
|
|
211
|
+
};
|
|
212
|
+
const queue = [];
|
|
213
|
+
const seedFrom = (points) => {
|
|
214
|
+
const every = Math.max(1, Math.round(spacing / step / 2));
|
|
215
|
+
for (let i = 1;i < points.length - 1; i += every) {
|
|
216
|
+
const [ax, ay] = points[i - 1];
|
|
217
|
+
const [bx, by] = points[i + 1];
|
|
218
|
+
const length = Math.hypot(bx - ax, by - ay) || 1;
|
|
219
|
+
const [nx, ny] = [-(by - ay) / length, (bx - ax) / length];
|
|
220
|
+
const [x, y] = points[i];
|
|
221
|
+
queue.push([x + nx * spacing, y + ny * spacing], [x - nx * spacing, y - ny * spacing]);
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
const attempt = (seed) => {
|
|
225
|
+
if (!free(seed[0], seed[1], -1, 0))
|
|
226
|
+
return;
|
|
227
|
+
const points = trace(seed);
|
|
228
|
+
if (!points)
|
|
229
|
+
return;
|
|
230
|
+
lines.push(points);
|
|
231
|
+
seedFrom(points);
|
|
232
|
+
};
|
|
233
|
+
let next = 0;
|
|
234
|
+
const drain = () => {
|
|
235
|
+
while (next < queue.length)
|
|
236
|
+
attempt(queue[next++]);
|
|
237
|
+
};
|
|
238
|
+
queue.push([s.centre[0], s.centre[1] - spacing]);
|
|
239
|
+
drain();
|
|
240
|
+
const sweep = [];
|
|
241
|
+
for (let x = FRAME.cx - FRAME.rx;x <= FRAME.cx + FRAME.rx; x += spacing / 2)
|
|
242
|
+
for (let y = FRAME.cy - FRAME.ry;y <= FRAME.cy + FRAME.ry; y += spacing / 2)
|
|
243
|
+
if (outlineRadius(x, y) <= OUTLINE_LIMIT)
|
|
244
|
+
sweep.push([x, y]);
|
|
245
|
+
const distance = ([x, y]) => Math.hypot(x - s.centre[0], y - s.centre[1]);
|
|
246
|
+
sweep.sort((a, b) => distance(a) - distance(b) || a[0] - b[0] || a[1] - b[1]);
|
|
247
|
+
for (const seed of sweep) {
|
|
248
|
+
attempt(seed);
|
|
249
|
+
drain();
|
|
250
|
+
}
|
|
251
|
+
return orderFromCore(lines, s.centre);
|
|
252
|
+
}
|
|
253
|
+
function orderFromCore(lines, [cx, cy]) {
|
|
254
|
+
const reach = (line) => Math.min(...line.map(([x, y]) => Math.hypot(x - cx, y - cy)));
|
|
255
|
+
const swept = (line) => {
|
|
256
|
+
let total = 0;
|
|
257
|
+
for (let i = 1;i < line.length; i++) {
|
|
258
|
+
let d = Math.atan2(line[i][1] - cy, line[i][0] - cx) - Math.atan2(line[i - 1][1] - cy, line[i - 1][0] - cx);
|
|
259
|
+
if (d > Math.PI)
|
|
260
|
+
d -= 2 * Math.PI;
|
|
261
|
+
if (d < -Math.PI)
|
|
262
|
+
d += 2 * Math.PI;
|
|
263
|
+
total += d;
|
|
264
|
+
}
|
|
265
|
+
return total;
|
|
266
|
+
};
|
|
267
|
+
return lines.map((line) => ({ line: swept(line) < 0 ? [...line].reverse() : line, reach: reach(line) })).sort((a, b) => a.reach - b.reach || a.line[0][0] - b.line[0][0] || a.line[0][1] - b.line[0][1]).map(({ line }) => line);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// temp/src/slots.ts
|
|
271
|
+
function polylineLength(points) {
|
|
272
|
+
let length = 0;
|
|
273
|
+
for (let i = 1;i < points.length; i++)
|
|
274
|
+
length += Math.hypot(points[i][0] - points[i - 1][0], points[i][1] - points[i - 1][1]);
|
|
275
|
+
return length;
|
|
276
|
+
}
|
|
277
|
+
function allocate(lengths, count) {
|
|
278
|
+
const total = lengths.reduce((sum, length) => sum + length, 0);
|
|
279
|
+
if (total === 0 || count === 0)
|
|
280
|
+
return lengths.map(() => 0);
|
|
281
|
+
const average = total / count;
|
|
282
|
+
const counts = lengths.map((length) => Math.floor(length / average));
|
|
283
|
+
for (let left = count - counts.reduce((sum, n) => sum + n, 0);left > 0; left--) {
|
|
284
|
+
let best = 0;
|
|
285
|
+
for (let i = 1;i < lengths.length; i++)
|
|
286
|
+
if (lengths[i] / (counts[i] + 1) > lengths[best] / (counts[best] + 1))
|
|
287
|
+
best = i;
|
|
288
|
+
counts[best] += 1;
|
|
289
|
+
}
|
|
290
|
+
return counts;
|
|
291
|
+
}
|
|
292
|
+
function pointAt(line, at) {
|
|
293
|
+
let walked = 0;
|
|
294
|
+
for (let i = 1;i < line.length; i++) {
|
|
295
|
+
const [x0, y0] = line[i - 1];
|
|
296
|
+
const [x1, y1] = line[i];
|
|
297
|
+
const segment = Math.hypot(x1 - x0, y1 - y0);
|
|
298
|
+
if (at <= walked + segment || i === line.length - 1) {
|
|
299
|
+
const f = segment === 0 ? 0 : Math.min(1, (at - walked) / segment);
|
|
300
|
+
return {
|
|
301
|
+
x: x0 + (x1 - x0) * f,
|
|
302
|
+
y: y0 + (y1 - y0) * f,
|
|
303
|
+
rotation: Math.atan2(y1 - y0, x1 - x0) * 180 / Math.PI
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
walked += segment;
|
|
307
|
+
}
|
|
308
|
+
const [x, y] = line[0];
|
|
309
|
+
return { x, y, rotation: 0 };
|
|
310
|
+
}
|
|
311
|
+
function slotsAlong(lines, count) {
|
|
312
|
+
const lengths = lines.map(polylineLength);
|
|
313
|
+
const counts = allocate(lengths, count);
|
|
314
|
+
const slots = [];
|
|
315
|
+
let pitch = Infinity;
|
|
316
|
+
lines.forEach((line, k) => {
|
|
317
|
+
const n = counts[k];
|
|
318
|
+
if (n === 0)
|
|
319
|
+
return;
|
|
320
|
+
const step = lengths[k] / n;
|
|
321
|
+
if (n > 1)
|
|
322
|
+
pitch = Math.min(pitch, step);
|
|
323
|
+
for (let i = 0;i < n; i++)
|
|
324
|
+
slots.push(pointAt(line, (i + 0.5) * step));
|
|
325
|
+
});
|
|
326
|
+
return { slots, pitch };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// temp/src/layout.ts
|
|
330
|
+
import { UNKNOWN_CATEGORY_ID } from "@data-profile/core";
|
|
331
|
+
import { DataError } from "@data-profile/core";
|
|
332
|
+
var DEFAULTS = { seed: "fingerprint", maxMarks: 600, minRidges: 6 };
|
|
333
|
+
var MARK_FILL = 0.85;
|
|
334
|
+
var positive = (value) => value !== undefined && Number.isFinite(value) && value >= 1 ? Math.floor(value) : undefined;
|
|
335
|
+
var ridgeCache = new Map;
|
|
336
|
+
function cachedRidges(form, spacing) {
|
|
337
|
+
const key = `${JSON.stringify(form)}:${spacing}`;
|
|
338
|
+
const hit = ridgeCache.get(key);
|
|
339
|
+
if (hit)
|
|
340
|
+
return hit;
|
|
341
|
+
const traced = ridges(form, spacing);
|
|
342
|
+
ridgeCache.set(key, traced);
|
|
343
|
+
if (ridgeCache.size > 8)
|
|
344
|
+
ridgeCache.delete(ridgeCache.keys().next().value);
|
|
345
|
+
return traced;
|
|
346
|
+
}
|
|
347
|
+
function layoutFingerprint(input) {
|
|
348
|
+
const { items, dimensions, encoding } = input;
|
|
349
|
+
const seed = input.seed ?? DEFAULTS.seed;
|
|
350
|
+
const maxMarks = positive(input.maxMarks) ?? DEFAULTS.maxMarks;
|
|
351
|
+
const minRidges = positive(input.minRidges) ?? DEFAULTS.minRidges;
|
|
352
|
+
const form = resolveForm(seed, input.form);
|
|
353
|
+
const { marks, ...profile } = encodeProfile({ items, dimensions, encoding, maxMarks });
|
|
354
|
+
const count = Math.max(1, marks.length);
|
|
355
|
+
const widest = FRAME.rx / minRidges;
|
|
356
|
+
let spacing = Math.min(widest, Math.sqrt(Math.PI * FRAME.rx * FRAME.ry * 0.85 / count));
|
|
357
|
+
let lines = cachedRidges(form, spacing);
|
|
358
|
+
if (marks.length > 0) {
|
|
359
|
+
const traced = lines.reduce((sum, line) => sum + polylineLength(line), 0);
|
|
360
|
+
const corrected = Math.min(widest, Math.sqrt(traced * spacing / count));
|
|
361
|
+
if (Math.abs(corrected - spacing) / spacing > 0.03) {
|
|
362
|
+
spacing = corrected;
|
|
363
|
+
lines = cachedRidges(form, spacing);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const { slots, pitch } = slotsAlong(lines, marks.length);
|
|
367
|
+
const footprint = Math.min(pitch, spacing) * MARK_FILL;
|
|
368
|
+
const heaviest = Math.max(0, ...marks.map((mark) => mark.weight));
|
|
369
|
+
const nodes = marks.map((mark, i) => {
|
|
370
|
+
const slot = slots[i];
|
|
371
|
+
return {
|
|
372
|
+
...mark,
|
|
373
|
+
x: slot.x,
|
|
374
|
+
y: slot.y,
|
|
375
|
+
rotation: slot.rotation,
|
|
376
|
+
size: footprint * Math.sqrt(mark.weight / heaviest)
|
|
377
|
+
};
|
|
378
|
+
});
|
|
379
|
+
return {
|
|
380
|
+
width: FRAME.width,
|
|
381
|
+
height: FRAME.height,
|
|
382
|
+
...profile,
|
|
383
|
+
nodes,
|
|
384
|
+
ridges: lines,
|
|
385
|
+
form,
|
|
386
|
+
ridgeSpacing: spacing
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
function emptyLayout() {
|
|
390
|
+
return {
|
|
391
|
+
width: FRAME.width,
|
|
392
|
+
height: FRAME.height,
|
|
393
|
+
encoding: { arrange: "", color: null, shape: null, lightness: null },
|
|
394
|
+
nodes: [],
|
|
395
|
+
ridges: [],
|
|
396
|
+
form: resolveForm(DEFAULTS.seed),
|
|
397
|
+
ridgeSpacing: FRAME.rx / DEFAULTS.minRidges,
|
|
398
|
+
legends: [],
|
|
399
|
+
warnings: [],
|
|
400
|
+
itemsPerMark: 1,
|
|
401
|
+
itemCount: 0
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// temp/src/callouts.ts
|
|
406
|
+
var CALLOUT_GUTTER = 190;
|
|
407
|
+
var ANCHOR_CLEARANCE = 28;
|
|
408
|
+
var CALLOUT_HEIGHT = 96;
|
|
409
|
+
function placeCallouts(layout, annotations) {
|
|
410
|
+
const taken = [];
|
|
411
|
+
const anchored = annotations.flatMap((annotation) => {
|
|
412
|
+
const wanted = new Set(annotation.categoryIds);
|
|
413
|
+
const dimensionId = annotation.dimensionId ?? layout.encoding.arrange;
|
|
414
|
+
const marks = layout.nodes.filter((node) => wanted.has(node.categories[dimensionId] ?? ""));
|
|
415
|
+
if (marks.length === 0)
|
|
416
|
+
return [];
|
|
417
|
+
const cx = marks.reduce((sum, node) => sum + node.x, 0) / marks.length;
|
|
418
|
+
const cy = marks.reduce((sum, node) => sum + node.y, 0) / marks.length;
|
|
419
|
+
const byCloseness = [...marks].sort((a, b) => Math.hypot(a.x - cx, a.y - cy) - Math.hypot(b.x - cx, b.y - cy));
|
|
420
|
+
const clear = (node) => taken.every((t) => Math.hypot(node.x - t.x, node.y - t.y) >= ANCHOR_CLEARANCE);
|
|
421
|
+
const anchor = byCloseness.find(clear) ?? byCloseness[0];
|
|
422
|
+
taken.push(anchor);
|
|
423
|
+
const side = anchor.x < layout.width / 2 ? "left" : "right";
|
|
424
|
+
return [{ annotation, anchor: { x: anchor.x, y: anchor.y }, side }];
|
|
425
|
+
});
|
|
426
|
+
const placed = [];
|
|
427
|
+
for (const side of ["left", "right"]) {
|
|
428
|
+
const column = anchored.filter((c) => c.side === side).sort((a, b) => a.anchor.y - b.anchor.y);
|
|
429
|
+
const tops = [];
|
|
430
|
+
for (const callout of column)
|
|
431
|
+
tops.push(Math.max(callout.anchor.y - CALLOUT_HEIGHT / 3, (tops.at(-1) ?? -Infinity) + CALLOUT_HEIGHT));
|
|
432
|
+
const overflow = Math.max(0, (tops.at(-1) ?? 0) + CALLOUT_HEIGHT - layout.height);
|
|
433
|
+
column.forEach((callout, i) => placed.push({ ...callout, labelY: Math.max(0, tops[i] - overflow), number: 0 }));
|
|
434
|
+
}
|
|
435
|
+
return placed.map((callout, i) => ({ ...callout, number: i + 1 }));
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// temp/src/describe.ts
|
|
439
|
+
import { describeProfile } from "@data-profile/core";
|
|
440
|
+
function describeLayout(layout, unit = "items") {
|
|
441
|
+
return describeProfile(layout, unit);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export { formFromItems, layoutFingerprint, emptyLayout, UNKNOWN_CATEGORY_ID, DataError, CALLOUT_GUTTER, CALLOUT_HEIGHT, placeCallouts, describeLayout };
|
|
445
|
+
|
|
446
|
+
//# debugId=42AEEDAB3A19380564756E2164756E21
|
|
447
|
+
//# sourceMappingURL=chunk-y8jc1j2x.js.map
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../temp/src/form.ts", "../temp/src/layout.ts", "../temp/src/ridges.ts", "../temp/src/slots.ts", "../temp/src/callouts.ts", "../temp/src/describe.ts"],
|
|
4
|
+
"sourcesContent": [
|
|
5
|
+
"import { unitHash } from \"@data-profile/core\";\nimport type { FingerprintDimension, FingerprintItem, FingerprintForm } from \"./types\";\n\nconst clamp = (value: number, low: number, high: number) => Math.min(high, Math.max(low, value));\n/** Coarse steps, so a collection's fingerprint holds still as it grows by an item or two. */\nconst quantise = (value: number, step = 0.2) => Math.round(value / step) * step;\n\n/** A plausible shape from a seed alone: the default when the consumer gives none. */\nexport function formFromSeed(seed: string): FingerprintForm {\n const h = (key: string) => unitHash(`${seed}:${key}`);\n return {\n pattern: \"loop\",\n hand: h(\"hand\") < 0.5 ? -1 : 1,\n coreX: quantise(h(\"x\") * 2 - 1) * 0.5,\n coreY: quantise(h(\"y\") * 2 - 1) * 0.5,\n spread: quantise(h(\"spread\")),\n tilt: Math.round((h(\"tilt\") - 0.5) * 12),\n };\n}\n\nexport function resolveForm(seed: string, shape: Partial<FingerprintForm> = {}): FingerprintForm {\n const resolved = { ...formFromSeed(seed), ...shape };\n return {\n ...resolved,\n coreX: clamp(resolved.coreX, -1, 1),\n coreY: clamp(resolved.coreY, -1, 1),\n spread: clamp(resolved.spread, 0, 1),\n tilt: clamp(resolved.tilt, -10, 10),\n };\n}\n\n/** Mean position (0 to 1) of known values within a dimension's category order, or 0.5. */\nfunction lean(items: readonly FingerprintItem[], dimension: FingerprintDimension | undefined) {\n if (!dimension || dimension.categories.length < 2) return 0.5;\n const index = new Map(\n dimension.categories.map((c, i) => [c.id, i / (dimension.categories.length - 1)]),\n );\n let sum = 0;\n let n = 0;\n for (const item of items) {\n const at = index.get(item.values[dimension.id] ?? \"\");\n if (at !== undefined) {\n sum += at;\n n += 1;\n }\n }\n return n ? sum / n : 0.5;\n}\n\n/**\n * A shape that is the collection's own: derived only from the data, so the same collection always\n * draws the same fingerprint, and it changes slowly as the collection grows. It is a signature,\n * not a chart; say so beside it. The mapping, which a caption can state:\n *\n * - **Pattern.** An arch while there are fewer than 12 items; then a loop when one category of the\n * first dimension holds 40% or more of the items (a focused collection); otherwise a whorl.\n * - **Core across, and loop direction.** Where the second dimension's values sit, on average, in its\n * category order: earlier categories pull the core left, later ones right.\n * - **Core height.** The same for the third dimension.\n * - **Spread.** The share of all categories that have at least one item: broader reads looser.\n * - **Tilt.** From the seed only, so two equal collections still differ slightly.\n */\nexport function formFromItems(\n items: readonly FingerprintItem[],\n dimensions: readonly FingerprintDimension[],\n seed = \"fingerprint\",\n): FingerprintForm {\n const [first, second, third] = dimensions;\n let top = 0;\n if (first) {\n const counts = new Map<string, number>();\n for (const item of items) {\n const value = item.values[first.id];\n if (value != null) counts.set(value, (counts.get(value) ?? 0) + 1);\n }\n top = items.length ? Math.max(0, ...counts.values()) / items.length : 0;\n }\n const total = dimensions.reduce((sum, d) => sum + d.categories.length, 0);\n const used = dimensions.reduce(\n (sum, d) =>\n sum + d.categories.filter((c) => items.some((item) => item.values[d.id] === c.id)).length,\n 0,\n );\n const across = lean(items, second);\n return resolveForm(seed, {\n pattern: items.length < 12 ? \"arch\" : top >= 0.4 ? \"loop\" : \"whorl\",\n hand: across < 0.5 ? -1 : 1,\n coreX: quantise((across - 0.5) * 2),\n coreY: quantise((lean(items, third) - 0.5) * 2),\n spread: total ? quantise(used / total) : 0.5,\n });\n}\n",
|
|
6
|
+
"import { encodeProfile } from \"@data-profile/core\";\nimport { FRAME, ridges } from \"./ridges\";\nimport { resolveForm } from \"./form\";\nimport { polylineLength, slotsAlong } from \"./slots\";\nimport type {\n FingerprintForm,\n FingerprintLayout,\n FingerprintNode,\n LayoutInput,\n Point,\n} from \"./types\";\n\n/** The id of the category unknown values fall into. Reserved: a dimension may not use it. */\nexport { UNKNOWN_CATEGORY_ID } from \"@data-profile/core\";\n\n/**\n * Data the layout cannot draw truthfully: duplicate item ids, a weight that is not positive, a\n * reserved or duplicated category id, an encoding naming no dimension, or lightness on an unordered\n * dimension. `layoutFingerprint` throws it; `useFingerprint` returns it as `error`. Core's\n * `DataError`.\n */\nexport { DataError as FingerprintDataError } from \"@data-profile/core\";\n\n/**\n * 600 marks: the most SVG glides smoothly between views (measured in Chrome: 56-59 fps at 400,\n * 43-50 at 800, 16-27 at 1,500), and large enough that a mark stays legible at phone width.\n */\nconst DEFAULTS = { seed: \"fingerprint\", maxMarks: 600, minRidges: 6 } as const;\n/** A mark fills this share of the space between its neighbours. */\nconst MARK_FILL = 0.85;\n\n/** A usable count, or undefined for anything else (undefined, NaN, zero, negative). */\nconst positive = (value: number | undefined) =>\n value !== undefined && Number.isFinite(value) && value >= 1 ? Math.floor(value) : undefined;\n\n/**\n * Ridges depend only on the form and spacing, not on the active dimension, so switching lens\n * reuses them: the marks move, the ridges stay. A few recent results are kept; the layout stays a\n * pure function because equal inputs always gave equal ridges.\n */\nconst ridgeCache = new Map<string, Point[][]>();\nfunction cachedRidges(form: FingerprintForm, spacing: number): Point[][] {\n const key = `${JSON.stringify(form)}:${spacing}`;\n const hit = ridgeCache.get(key);\n if (hit) return hit;\n const traced = ridges(form, spacing);\n ridgeCache.set(key, traced);\n if (ridgeCache.size > 8) ridgeCache.delete(ridgeCache.keys().next().value!);\n return traced;\n}\n\n/**\n * Lays the items out along fingerprint ridges.\n *\n * `encoding` says which dimension arranges the marks and which drive colour, shape and lightness\n * (see `FingerprintEncoding`); left out, the first dimension does all but lightness. Items are\n * sorted by the arranging dimension's category order, then by each other encoded dimension's,\n * then by id, and laid along the ridges from the core outwards, so categories form runs. Mark\n * **area** is proportional to weight; positions only arrange.\n *\n * A pure function of its input: the same items, dimensions, encoding and options give the same\n * layout whatever order the items arrive in and whatever size it is drawn at. The ridges come from\n * `form` (see `formFromItems`). The items array is treated as immutable: work is cached against it.\n *\n * Above `maxMarks` items, consecutive items that agree on **every** encoded dimension share a mark\n * (`itemsPerMark` > 1), so a mark never misrepresents any item in it. Nothing is sampled away.\n */\nexport function layoutFingerprint(input: LayoutInput): FingerprintLayout {\n const { items, dimensions, encoding } = input;\n // Read field by field: spreading the input would let an explicit `undefined` (which callers pass\n // through when they have no value) replace a default, and an undefined cap means one mark per item.\n const seed = input.seed ?? DEFAULTS.seed;\n const maxMarks = positive(input.maxMarks) ?? DEFAULTS.maxMarks;\n const minRidges = positive(input.minRidges) ?? DEFAULTS.minRidges;\n const form = resolveForm(seed, input.form);\n const { marks, ...profile } = encodeProfile({ items, dimensions, encoding, maxMarks });\n\n // Ridges are spaced so that, laid end to end, they hold every mark about one spacing apart. The\n // first trace estimates their length from the frame's area; one retrace corrects the spacing from\n // the length actually traced, since ridge endings and the outline make that hard to predict.\n const count = Math.max(1, marks.length);\n const widest = FRAME.rx / minRidges;\n let spacing = Math.min(widest, Math.sqrt((Math.PI * FRAME.rx * FRAME.ry * 0.85) / count));\n let lines = cachedRidges(form, spacing);\n if (marks.length > 0) {\n const traced = lines.reduce((sum, line) => sum + polylineLength(line), 0);\n const corrected = Math.min(widest, Math.sqrt((traced * spacing) / count));\n if (Math.abs(corrected - spacing) / spacing > 0.03) {\n spacing = corrected;\n lines = cachedRidges(form, spacing);\n }\n }\n const { slots, pitch } = slotsAlong(lines, marks.length);\n const footprint = Math.min(pitch, spacing) * MARK_FILL;\n\n const heaviest = Math.max(0, ...marks.map((mark) => mark.weight));\n const nodes: FingerprintNode[] = marks.map((mark, i) => {\n const slot = slots[i]!;\n return {\n ...mark,\n x: slot.x,\n y: slot.y,\n rotation: slot.rotation,\n size: footprint * Math.sqrt(mark.weight / heaviest),\n };\n });\n\n return {\n width: FRAME.width,\n height: FRAME.height,\n ...profile,\n nodes,\n ridges: lines,\n form,\n ridgeSpacing: spacing,\n };\n}\n\n/** A layout with nothing in it: what `useFingerprint` shows while its data is invalid. */\nexport function emptyLayout(): FingerprintLayout {\n return {\n width: FRAME.width,\n height: FRAME.height,\n encoding: { arrange: \"\", color: null, shape: null, lightness: null },\n nodes: [],\n ridges: [],\n form: resolveForm(DEFAULTS.seed),\n ridgeSpacing: FRAME.rx / DEFAULTS.minRidges,\n legends: [],\n warnings: [],\n itemsPerMark: 1,\n itemCount: 0,\n };\n}\n",
|
|
7
|
+
"import type { FingerprintForm, Point } from \"./types\";\n\n/** The fixed frame every layout is drawn in: a portrait fingertip with a small margin. */\nexport const FRAME = { width: 400, height: 520, cx: 200, cy: 260, rx: 180, ry: 240 } as const;\n\nconst DEG = Math.PI / 180;\n\n/** Normalised radius of the fingertip outline through a point: 1 on the outline. */\nexport const outlineRadius = (x: number, y: number) =>\n Math.hypot((x - FRAME.cx) / FRAME.rx, (y - FRAME.cy) / FRAME.ry);\n\nconst smoothstep = (a: number, b: number, x: number) => {\n const t = Math.min(1, Math.max(0, (x - a) / (b - a)));\n return t * t * (3 - 2 * t);\n};\n\n/** Where the ridges curl (cores) and split three ways (deltas). */\nexport interface Singularities {\n centre: Point;\n cores: Point[];\n deltas: Point[];\n}\n\n/**\n * Places a shape's singular points. A loop has one core and one delta below it to one side; a\n * whorl two cores and a delta either side; an arch none.\n */\nexport function singularities(shape: FingerprintForm): Singularities {\n const { rx, ry, cx, cy } = FRAME;\n const centre: Point = [cx + shape.coreX * 0.12 * rx, cy - 0.1 * ry + shape.coreY * 0.1 * ry];\n const [x, y] = centre;\n if (shape.pattern === \"arch\") return { centre, cores: [], deltas: [] };\n if (shape.pattern === \"loop\")\n return {\n centre,\n cores: [centre],\n deltas: [\n [x + shape.hand * (0.4 + 0.25 * shape.spread) * rx, y + (0.38 + 0.12 * shape.spread) * ry],\n ],\n };\n const twist = 0.06 * rx;\n return {\n centre,\n cores: [\n [x - twist, y - shape.hand * 0.025 * ry],\n [x + twist, y + shape.hand * 0.025 * ry],\n ],\n deltas: [\n [x - (0.5 + 0.2 * shape.spread) * rx, y + 0.42 * ry],\n [x + (0.5 + 0.2 * shape.spread) * rx, y + 0.42 * ry],\n ],\n };\n}\n\n/**\n * Ridge direction at a point, in radians, modulo pi.\n *\n * Inside, the Sherlock-Monro model: half the sum of the angles to each core minus half the sum to\n * each delta, which is what makes ridges curl round a core and split at a delta. An arch has no\n * singular points, so its ridges follow a raised bump instead. Near the outline, and mostly over the\n * top, ridges bend to follow it, as they do round a fingertip.\n */\nexport function orientation(\n x: number,\n y: number,\n shape: FingerprintForm,\n s: Singularities,\n): number {\n const tilt = shape.tilt * DEG;\n let theta: number;\n if (shape.pattern === \"arch\") {\n const width = (0.35 + 0.25 * shape.spread) * FRAME.rx;\n const u = (x - s.centre[0]) / width;\n const height = 0.3 * FRAME.ry * Math.max(0, 1 - Math.abs(y - s.centre[1]) / FRAME.ry);\n theta = tilt + Math.atan(((2 * height * u) / width) * Math.exp(-u * u));\n } else {\n let sum = 0;\n for (const [px, py] of s.cores) sum += Math.atan2(y - py, x - px);\n for (const [px, py] of s.deltas) sum -= Math.atan2(y - py, x - px);\n theta = tilt + sum / 2;\n }\n // Blend towards the outline's tangent, as doubled angles so pi-periodic directions average.\n const r = outlineRadius(x, y);\n // Strongest over the top of the fingertip, easing to 40% below it: a step here kinks ridges.\n const over = smoothstep(FRAME.cy + 0.25 * FRAME.ry, FRAME.cy - 0.25 * FRAME.ry, y);\n const weight = smoothstep(0.68, 1, r) * (0.4 + 0.6 * over);\n if (weight === 0) return theta;\n const tangent = Math.atan2((x - FRAME.cx) / FRAME.rx ** 2, -(y - FRAME.cy) / FRAME.ry ** 2);\n const vx = (1 - weight) * Math.cos(2 * theta) + weight * Math.cos(2 * tangent);\n const vy = (1 - weight) * Math.sin(2 * theta) + weight * Math.sin(2 * tangent);\n return Math.atan2(vy, vx) / 2;\n}\n\n/** A spatial hash of every accepted ridge point, for the spacing checks. */\nclass PointGrid {\n private cells = new Map<number, { x: number; y: number; line: number; index: number }[]>();\n constructor(private size: number) {}\n private key = (gx: number, gy: number) => gx * 4096 + gy;\n /** Returns the cell key, so a rejected ridge can be taken out again cheaply. */\n add(x: number, y: number, line: number, index: number) {\n const k = this.key(Math.floor(x / this.size), Math.floor(y / this.size));\n const cell = this.cells.get(k);\n if (cell) cell.push({ x, y, line, index });\n else this.cells.set(k, [{ x, y, line, index }]);\n return k;\n }\n removeLine(line: number, keys: Iterable<number>) {\n for (const k of new Set(keys)) {\n const kept = (this.cells.get(k) ?? []).filter((p) => p.line !== line);\n if (kept.length) this.cells.set(k, kept);\n else this.cells.delete(k);\n }\n }\n /** True when a point closer than `distance` passes `counts`. `distance` must not exceed the cell size. */\n near(\n x: number,\n y: number,\n distance: number,\n counts: (p: { line: number; index: number }) => boolean,\n ) {\n const gx = Math.floor(x / this.size);\n const gy = Math.floor(y / this.size);\n for (let dx = -1; dx <= 1; dx++)\n for (let dy = -1; dy <= 1; dy++)\n for (const p of this.cells.get(this.key(gx + dx, gy + dy)) ?? [])\n if ((p.x - x) ** 2 + (p.y - y) ** 2 < distance ** 2 && counts(p)) return true;\n return false;\n }\n}\n\n/** Ridges stop this far inside the outline. */\nconst OUTLINE_LIMIT = 0.985;\n\n/**\n * Evenly spaced ridges along the shape's flow field (Jobard and Lefer's streamline placement),\n * core outwards. Each ridge is traced both ways from a seed until it would come within `spacing`\n * of another ridge, of a singular point, of a distant part of itself, or of the outline; new seeds\n * are tried one spacing either side of every accepted ridge. A dropped-out ridge end is a ridge\n * ending, exactly as in a real print.\n *\n * Every point of every ridge is at least about `spacing` from every other ridge, which is what lets\n * marks of that size sit on them without touching.\n */\nexport function ridges(shape: FingerprintForm, spacing: number): Point[][] {\n const s = singularities(shape);\n const step = spacing * 0.3;\n const selfGap = Math.ceil((3 * spacing) / step);\n const minLength = Math.ceil((2 * spacing) / step);\n const maxSteps = Math.ceil((4 * Math.PI * (FRAME.rx + FRAME.ry)) / step);\n const grid = new PointGrid(spacing);\n const lines: Point[][] = [];\n const singular = [...s.cores, ...s.deltas];\n\n const free = (x: number, y: number, line: number, index: number) =>\n outlineRadius(x, y) <= OUTLINE_LIMIT &&\n singular.every(([px, py]) => Math.hypot(x - px, y - py) > spacing * 0.6) &&\n !grid.near(x, y, spacing, (p) => p.line !== line || Math.abs(p.index - index) > selfGap);\n\n const direction = (x: number, y: number, previous: Point): Point => {\n const theta = orientation(x, y, shape, s);\n const v: Point = [Math.cos(theta), Math.sin(theta)];\n return v[0] * previous[0] + v[1] * previous[1] < 0 ? [-v[0], -v[1]] : v;\n };\n\n const trace = (seed: Point): Point[] | null => {\n const line = lines.length;\n const theta = orientation(seed[0], seed[1], shape, s);\n const keys = [grid.add(seed[0], seed[1], line, 0)];\n const halves = ([1, -1] as const).map((sign) => {\n const points: Point[] = [];\n let p = seed;\n let previous: Point = [sign * Math.cos(theta), sign * Math.sin(theta)];\n for (let i = 1; i <= maxSteps; i++) {\n // Second-order Runge-Kutta: sample the direction half a step ahead.\n const v1 = direction(p[0], p[1], previous);\n const v2 = direction(p[0] + (v1[0] * step) / 2, p[1] + (v1[1] * step) / 2, v1);\n // A ridge turns gently; a sharp turn in one step means the field folds here, so end it.\n if (v2[0] * previous[0] + v2[1] * previous[1] < 0.8) break;\n const q: Point = [p[0] + v2[0] * step, p[1] + v2[1] * step];\n if (!free(q[0], q[1], line, sign * i)) break;\n keys.push(grid.add(q[0], q[1], line, sign * i));\n points.push(q);\n p = q;\n previous = v2;\n }\n return points;\n });\n const points = [...halves[1]!.reverse(), seed, ...halves[0]!];\n if (points.length < minLength) {\n grid.removeLine(line, keys);\n return null;\n }\n return points;\n };\n\n const queue: Point[] = [];\n const seedFrom = (points: Point[]) => {\n const every = Math.max(1, Math.round(spacing / step / 2));\n for (let i = 1; i < points.length - 1; i += every) {\n const [ax, ay] = points[i - 1]!;\n const [bx, by] = points[i + 1]!;\n const length = Math.hypot(bx - ax, by - ay) || 1;\n const [nx, ny] = [-(by - ay) / length, (bx - ax) / length];\n const [x, y] = points[i]!;\n queue.push([x + nx * spacing, y + ny * spacing], [x - nx * spacing, y - ny * spacing]);\n }\n };\n const attempt = (seed: Point) => {\n if (!free(seed[0], seed[1], -1, 0)) return;\n const points = trace(seed);\n if (!points) return;\n lines.push(points);\n seedFrom(points);\n };\n let next = 0;\n const drain = () => {\n while (next < queue.length) attempt(queue[next++]!);\n };\n\n // Start just above the core and grow outwards; then sweep for any region the growth missed.\n queue.push([s.centre[0], s.centre[1] - spacing]);\n drain();\n const sweep: Point[] = [];\n for (let x = FRAME.cx - FRAME.rx; x <= FRAME.cx + FRAME.rx; x += spacing / 2)\n for (let y = FRAME.cy - FRAME.ry; y <= FRAME.cy + FRAME.ry; y += spacing / 2)\n if (outlineRadius(x, y) <= OUTLINE_LIMIT) sweep.push([x, y]);\n const distance = ([x, y]: Point) => Math.hypot(x - s.centre[0], y - s.centre[1]);\n sweep.sort((a, b) => distance(a) - distance(b) || a[0] - b[0] || a[1] - b[1]);\n for (const seed of sweep) {\n attempt(seed);\n drain();\n }\n\n return orderFromCore(lines, s.centre);\n}\n\n/**\n * Ridges sorted by how close they come to the core, each running clockwise round it, so marks laid\n * along them in turn form runs from the core outwards.\n */\nfunction orderFromCore(lines: Point[][], [cx, cy]: Point): Point[][] {\n const reach = (line: Point[]) => Math.min(...line.map(([x, y]) => Math.hypot(x - cx, y - cy)));\n const swept = (line: Point[]) => {\n let total = 0;\n for (let i = 1; i < line.length; i++) {\n let d =\n Math.atan2(line[i]![1] - cy, line[i]![0] - cx) -\n Math.atan2(line[i - 1]![1] - cy, line[i - 1]![0] - cx);\n if (d > Math.PI) d -= 2 * Math.PI;\n if (d < -Math.PI) d += 2 * Math.PI;\n total += d;\n }\n return total;\n };\n return lines\n .map((line) => ({ line: swept(line) < 0 ? [...line].reverse() : line, reach: reach(line) }))\n .sort(\n (a, b) => a.reach - b.reach || a.line[0]![0] - b.line[0]![0] || a.line[0]![1] - b.line[0]![1],\n )\n .map(({ line }) => line);\n}\n",
|
|
8
|
+
"import type { Point } from \"./types\";\n\nexport function polylineLength(points: readonly Point[]): number {\n let length = 0;\n for (let i = 1; i < points.length; i++)\n length += Math.hypot(points[i]![0] - points[i - 1]![0], points[i]![1] - points[i - 1]![1]);\n return length;\n}\n\n/**\n * Splits `count` across ridges so the most crowded ridge is as uncrowded as possible: every ridge\n * first gets as many slots as fit at the average spacing, then each remaining slot goes to the\n * ridge whose spacing it shrinks least (the D'Hondt rule). Ties go to the earlier ridge, so the\n * result is stable.\n */\nfunction allocate(lengths: readonly number[], count: number): number[] {\n const total = lengths.reduce((sum, length) => sum + length, 0);\n if (total === 0 || count === 0) return lengths.map(() => 0);\n const average = total / count;\n const counts = lengths.map((length) => Math.floor(length / average));\n for (let left = count - counts.reduce((sum, n) => sum + n, 0); left > 0; left--) {\n let best = 0;\n for (let i = 1; i < lengths.length; i++)\n if (lengths[i]! / (counts[i]! + 1) > lengths[best]! / (counts[best]! + 1)) best = i;\n counts[best]! += 1;\n }\n return counts;\n}\n\n/** The point and direction (degrees) at arc length `at` along a polyline. */\nfunction pointAt(line: readonly Point[], at: number) {\n let walked = 0;\n for (let i = 1; i < line.length; i++) {\n const [x0, y0] = line[i - 1]!;\n const [x1, y1] = line[i]!;\n const segment = Math.hypot(x1 - x0, y1 - y0);\n if (at <= walked + segment || i === line.length - 1) {\n const f = segment === 0 ? 0 : Math.min(1, (at - walked) / segment);\n return {\n x: x0 + (x1 - x0) * f,\n y: y0 + (y1 - y0) * f,\n rotation: (Math.atan2(y1 - y0, x1 - x0) * 180) / Math.PI,\n };\n }\n walked += segment;\n }\n const [x, y] = line[0]!;\n return { x, y, rotation: 0 };\n}\n\n/**\n * `count` positions along the ridges, in ridge order. Each ridge gets slots by `allocate` and spaces them evenly over its whole length, so a closed ring has no crowded seam where\n * its end meets its start. `pitch` is the smallest spacing between\n * neighbours on any ridge.\n */\nexport function slotsAlong(lines: readonly Point[][], count: number) {\n const lengths = lines.map(polylineLength);\n const counts = allocate(lengths, count);\n const slots: { x: number; y: number; rotation: number }[] = [];\n let pitch = Infinity;\n lines.forEach((line, k) => {\n const n = counts[k]!;\n if (n === 0) return;\n const step = lengths[k]! / n;\n // A ridge with one slot has no neighbour along it, so its length says nothing about crowding.\n if (n > 1) pitch = Math.min(pitch, step);\n for (let i = 0; i < n; i++) slots.push(pointAt(line, (i + 0.5) * step));\n });\n // Infinity when no ridge holds two slots: nothing along a ridge then limits mark size.\n return { slots, pitch };\n}\n",
|
|
9
|
+
"import type { FingerprintAnnotation, FingerprintLayout } from \"./types\";\n\n/** Horizontal room either side of the print for callout labels, in layout units. */\nexport const CALLOUT_GUTTER = 190;\n/** How far apart two callouts' anchors stay: a numbered marker is 22 units across. */\nconst ANCHOR_CLEARANCE = 28;\n/** Vertical room one callout label takes, in layout units. */\nexport const CALLOUT_HEIGHT = 96;\n\n/** Where `placeCallouts` puts one annotation, in the layout's frame. */\nexport interface PlacedCallout {\n /** The annotation placed. */\n annotation: FingerprintAnnotation;\n /** The mark the leader points at: the named categories' mark nearest their centre. */\n anchor: { x: number; y: number };\n /** Which gutter the label sits in: the side of the print its anchor is on. */\n side: \"left\" | \"right\";\n /** Top of the label, in layout units. */\n labelY: number;\n /** 1-based, in reading order: left side top to bottom, then right. */\n number: number;\n}\n\n/**\n * Where each callout goes. Its anchor is a real mark of the categories it names, so the leader\n * never points at empty paper; the label sits in the gutter on the anchor's side, as level with it\n * as the other labels allow. Annotations whose categories have no marks are left out.\n */\nexport function placeCallouts(\n layout: FingerprintLayout,\n annotations: readonly FingerprintAnnotation[],\n): PlacedCallout[] {\n // Anchors already taken: a later callout's anchor keeps this far from them, so numbered markers\n // never hide each other.\n const taken: { x: number; y: number }[] = [];\n const anchored = annotations.flatMap((annotation) => {\n const wanted = new Set(annotation.categoryIds);\n const dimensionId = annotation.dimensionId ?? layout.encoding.arrange;\n const marks = layout.nodes.filter((node) => wanted.has(node.categories[dimensionId] ?? \"\"));\n if (marks.length === 0) return [];\n const cx = marks.reduce((sum, node) => sum + node.x, 0) / marks.length;\n const cy = marks.reduce((sum, node) => sum + node.y, 0) / marks.length;\n const byCloseness = [...marks].sort(\n (a, b) => Math.hypot(a.x - cx, a.y - cy) - Math.hypot(b.x - cx, b.y - cy),\n );\n const clear = (node: { x: number; y: number }) =>\n taken.every((t) => Math.hypot(node.x - t.x, node.y - t.y) >= ANCHOR_CLEARANCE);\n const anchor = byCloseness.find(clear) ?? byCloseness[0]!;\n taken.push(anchor);\n const side: PlacedCallout[\"side\"] = anchor.x < layout.width / 2 ? \"left\" : \"right\";\n return [{ annotation, anchor: { x: anchor.x, y: anchor.y }, side }];\n });\n\n const placed: PlacedCallout[] = [];\n for (const side of [\"left\", \"right\"] as const) {\n const column = anchored.filter((c) => c.side === side).sort((a, b) => a.anchor.y - b.anchor.y);\n // Level with the anchor where possible, pushed down to clear the label above, then the whole\n // column pulled back up if the last label would run off the bottom.\n const tops: number[] = [];\n for (const callout of column)\n tops.push(\n Math.max(\n callout.anchor.y - CALLOUT_HEIGHT / 3,\n (tops.at(-1) ?? -Infinity) + CALLOUT_HEIGHT,\n ),\n );\n const overflow = Math.max(0, (tops.at(-1) ?? 0) + CALLOUT_HEIGHT - layout.height);\n column.forEach((callout, i) =>\n placed.push({ ...callout, labelY: Math.max(0, tops[i]! - overflow), number: 0 }),\n );\n }\n return placed.map((callout, i) => ({ ...callout, number: i + 1 }));\n}\n",
|
|
10
|
+
"import { describeProfile } from \"@data-profile/core\";\nimport type { FingerprintLayout } from \"./types\";\n\n/**\n * A plain-language summary of what a layout shows, for a screen reader or a caption: the total, the\n * largest categories of the arranging dimension with their counts, which dimension each visual\n * property shows when they differ, and how many items each mark stands for. Every number in it is\n * one the legends also show. Core's `describeProfile`.\n */\nexport function describeLayout(layout: FingerprintLayout, unit = \"items\"): string {\n return describeProfile(layout, unit);\n}\n"
|
|
11
|
+
],
|
|
12
|
+
"mappings": "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",
|
|
13
|
+
"debugId": "42AEEDAB3A19380564756E2164756E21",
|
|
14
|
+
"names": []
|
|
15
|
+
}
|