@macrostrat/column-views 3.8.0 → 3.9.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 +100 -0
- package/dist/animated-age-window.cjs +51 -0
- package/dist/animated-age-window.cjs.map +1 -0
- package/dist/animated-age-window.d.ts +51 -0
- package/dist/animated-age-window.js +51 -0
- package/dist/animated-age-window.js.map +1 -0
- package/dist/column.cjs +11 -1
- package/dist/column.cjs.map +1 -1
- package/dist/column.d.ts +11 -0
- package/dist/column.js +11 -1
- package/dist/column.js.map +1 -1
- package/dist/correlation-chart/main.cjs +24 -12
- package/dist/correlation-chart/main.cjs.map +1 -1
- package/dist/correlation-chart/main.d.ts +8 -1
- package/dist/correlation-chart/main.js +24 -12
- package/dist/correlation-chart/main.js.map +1 -1
- package/dist/data-provider/store.cjs +17 -4
- package/dist/data-provider/store.cjs.map +1 -1
- package/dist/data-provider/store.d.ts +6 -1
- package/dist/data-provider/store.js +17 -4
- package/dist/data-provider/store.js.map +1 -1
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/prepare-units/composite-scale.cjs +94 -8
- package/dist/prepare-units/composite-scale.cjs.map +1 -1
- package/dist/prepare-units/composite-scale.d.ts +39 -0
- package/dist/prepare-units/composite-scale.js +96 -10
- package/dist/prepare-units/composite-scale.js.map +1 -1
- package/dist/prepare-units/helpers.cjs +0 -1
- package/dist/prepare-units/helpers.cjs.map +1 -1
- package/dist/prepare-units/helpers.js +0 -1
- package/dist/prepare-units/helpers.js.map +1 -1
- package/dist/prepare-units/index.cjs +33 -12
- package/dist/prepare-units/index.cjs.map +1 -1
- package/dist/prepare-units/index.js +34 -13
- package/dist/prepare-units/index.js.map +1 -1
- package/dist/prepare-units/types.cjs.map +1 -1
- package/dist/prepare-units/types.d.ts +13 -0
- package/dist/prepare-units/types.js.map +1 -1
- package/dist/section.cjs +4 -2
- package/dist/section.cjs.map +1 -1
- package/dist/section.d.ts +4 -1
- package/dist/section.js +4 -2
- package/dist/section.js.map +1 -1
- package/dist/units/boxes.cjs +19 -36
- package/dist/units/boxes.cjs.map +1 -1
- package/dist/units/boxes.js +21 -38
- package/dist/units/boxes.js.map +1 -1
- package/dist/units/composite.cjs +12 -12
- package/dist/units/composite.cjs.map +1 -1
- package/dist/units/composite.js +12 -12
- package/dist/units/composite.js.map +1 -1
- package/package.json +6 -6
- package/src/animated-age-window.ts +120 -0
- package/src/column.ts +24 -0
- package/src/correlation-chart/main.ts +30 -8
- package/src/data-provider/store.ts +21 -2
- package/src/index.ts +1 -0
- package/src/prepare-units/composite-scale.ts +210 -10
- package/src/prepare-units/helpers.ts +0 -2
- package/src/prepare-units/index.ts +68 -14
- package/src/prepare-units/types.ts +13 -0
- package/src/section.ts +6 -0
- package/src/units/boxes.ts +30 -52
- package/src/units/composite.ts +21 -12
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ColumnAxisType } from "@macrostrat/column-components";
|
|
2
|
-
import { ensureArray, getUnitHeightRange } from "./utils";
|
|
2
|
+
import { agesOverlap, ensureArray, getUnitHeightRange } from "./utils";
|
|
3
3
|
import { ScaleContinuousNumeric, scaleLinear } from "d3-scale";
|
|
4
4
|
import { UnitLong } from "@macrostrat/api-types";
|
|
5
5
|
import { buildHybridScale } from "./dynamic-scales";
|
|
@@ -28,10 +28,15 @@ export function buildCompositeScaleInfo(
|
|
|
28
28
|
let lastSectionTopHeight = 0;
|
|
29
29
|
|
|
30
30
|
const packages2: PackageScaleLayoutData[] = [];
|
|
31
|
-
for (
|
|
31
|
+
for (let i = 0; i < inputScales.length; i++) {
|
|
32
|
+
const group = inputScales[i];
|
|
32
33
|
const { domain, scale } = group;
|
|
33
34
|
const [b_age, t_age] = domain;
|
|
34
|
-
|
|
35
|
+
// Positionally stable key: packages are always emitted in age order, so the
|
|
36
|
+
// index is a stable identity. An age-based key (`package-${b_age}-${t_age}`)
|
|
37
|
+
// changes every frame while the age window animates, forcing every package,
|
|
38
|
+
// column, and timescale slice in the correlation chart to remount each frame.
|
|
39
|
+
const key = `package-${i}`;
|
|
35
40
|
|
|
36
41
|
packages2.push({
|
|
37
42
|
...createPackageScale(group, totalHeight),
|
|
@@ -132,6 +137,7 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
132
137
|
minPixelScale = 0.2,
|
|
133
138
|
axisType,
|
|
134
139
|
minSectionHeight,
|
|
140
|
+
visibleWindow,
|
|
135
141
|
scale,
|
|
136
142
|
hybridScale,
|
|
137
143
|
} = opts;
|
|
@@ -159,7 +165,7 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
159
165
|
|
|
160
166
|
if (scale == null) {
|
|
161
167
|
if (_pixelScale == null) {
|
|
162
|
-
const avgAgeRange = findAverageUnitHeight(data, axisType);
|
|
168
|
+
const avgAgeRange = findAverageUnitHeight(data, axisType, visibleWindow);
|
|
163
169
|
// Get pixel height necessary to render average unit at target height
|
|
164
170
|
_pixelScale = Math.max(targetUnitHeight / avgAgeRange, minPixelScale);
|
|
165
171
|
|
|
@@ -171,7 +177,11 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
171
177
|
}
|
|
172
178
|
|
|
173
179
|
let height = dAge * _pixelScale;
|
|
174
|
-
// If height is less than minSectionHeight, set it to minSectionHeight
|
|
180
|
+
// If height is less than minSectionHeight, set it to minSectionHeight.
|
|
181
|
+
// Sections reach here at their *full* extent (the rendered window is applied
|
|
182
|
+
// afterwards, by `trimSectionsToWindow`), so this floor only ever inflates a
|
|
183
|
+
// genuinely small section — which is what it's for — and never a sliver that
|
|
184
|
+
// the window happens to cut.
|
|
175
185
|
const _minSectionHeight = minSectionHeight ?? targetUnitHeight ?? 0;
|
|
176
186
|
pixelHeight = Math.max(height, _minSectionHeight);
|
|
177
187
|
_pixelScale = pixelHeight / dAge;
|
|
@@ -186,6 +196,168 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
186
196
|
);
|
|
187
197
|
}
|
|
188
198
|
|
|
199
|
+
/**
|
|
200
|
+
* The density each section will actually render at once the window is applied.
|
|
201
|
+
*
|
|
202
|
+
* A section the window cuts short is still being *looked at*, so
|
|
203
|
+
* `minSectionHeight` applies to what remains — it's honored by expanding the
|
|
204
|
+
* section's scale, not by showing more of it. Resolving this **before** padding
|
|
205
|
+
* matters: padding is a pixel margin measured at the density it will render at,
|
|
206
|
+
* and a section stretched to the floor afterwards would multiply the margin
|
|
207
|
+
* along with everything else (a 20 px margin in a band stretched 4× renders at
|
|
208
|
+
* 80 px, and asymmetrically so whenever one side is truncated).
|
|
209
|
+
*
|
|
210
|
+
* Sections the window leaves whole keep their laid-out density — they already
|
|
211
|
+
* met the floor when they were sized.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveWindowScales<T extends UnitLong>(
|
|
214
|
+
sections: SectionInfoWithScale<T>[],
|
|
215
|
+
focalWindow: [number, number],
|
|
216
|
+
floor: number,
|
|
217
|
+
): Map<SectionInfoWithScale<T>, number> {
|
|
218
|
+
const [b_focal, t_focal] = focalWindow;
|
|
219
|
+
const scales = new Map<SectionInfoWithScale<T>, number>();
|
|
220
|
+
|
|
221
|
+
for (const section of sections) {
|
|
222
|
+
const t_age = Math.max(section.t_age, t_focal);
|
|
223
|
+
const b_age = Math.min(section.b_age, b_focal);
|
|
224
|
+
// Sections outside the focal window are visible only as padding; they keep
|
|
225
|
+
// their own density and render exactly the pixels asked for.
|
|
226
|
+
if (b_age <= t_age) continue;
|
|
227
|
+
|
|
228
|
+
const natural = section.scaleInfo.pixelScale;
|
|
229
|
+
if (!(natural > 0)) continue;
|
|
230
|
+
|
|
231
|
+
const isClipped = b_age < section.b_age || t_age > section.t_age;
|
|
232
|
+
const needed = isClipped && floor > 0 ? floor / (b_age - t_age) : 0;
|
|
233
|
+
scales.set(section, Math.max(natural, needed));
|
|
234
|
+
}
|
|
235
|
+
return scales;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Expand an age window outward by `padding` **pixels** of column, spending the
|
|
240
|
+
* budget against sections that are already laid out at full extent, at the
|
|
241
|
+
* densities they will finally render at (`scaleFor`).
|
|
242
|
+
*
|
|
243
|
+
* Because the layout is done, this is exact rather than predicted: each section
|
|
244
|
+
* contributes its own real pixel height. Padding collapses when the column runs
|
|
245
|
+
* out on that side.
|
|
246
|
+
*
|
|
247
|
+
* Unconformities between sections cost nothing. They render at a fixed height
|
|
248
|
+
* that's typically larger than the padding itself (30–60 px), so charging them
|
|
249
|
+
* would mean padding could never reach across a gap — which is the case it
|
|
250
|
+
* exists for: revealing the neighboring section keeps its timescale intervals
|
|
251
|
+
* navigable. The padding is a budget of neighboring *content*.
|
|
252
|
+
*/
|
|
253
|
+
export function padWindowByPixels<T extends UnitLong>(
|
|
254
|
+
sections: SectionInfoWithScale<T>[],
|
|
255
|
+
window: [number, number],
|
|
256
|
+
padding: number,
|
|
257
|
+
scaleFor: (section: SectionInfoWithScale<T>) => number,
|
|
258
|
+
): [number, number] {
|
|
259
|
+
if (!(padding > 0) || sections.length === 0) return window;
|
|
260
|
+
const [b_age, t_age] = window;
|
|
261
|
+
return [
|
|
262
|
+
spendOutward(sections, b_age, 1, padding, scaleFor),
|
|
263
|
+
spendOutward(sections, t_age, -1, padding, scaleFor),
|
|
264
|
+
];
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** Walk out from one edge of the window through laid-out sections, spending a
|
|
268
|
+
* pixel budget, and return the age it lands on. `direction` is +1 toward older
|
|
269
|
+
* ages, -1 toward younger. */
|
|
270
|
+
function spendOutward<T extends UnitLong>(
|
|
271
|
+
sections: SectionInfoWithScale<T>[],
|
|
272
|
+
edge: number,
|
|
273
|
+
direction: 1 | -1,
|
|
274
|
+
padding: number,
|
|
275
|
+
scaleFor: (section: SectionInfoWithScale<T>) => number,
|
|
276
|
+
): number {
|
|
277
|
+
// Sections ordered outward from the edge, keeping only what lies beyond it.
|
|
278
|
+
const beyond = sections
|
|
279
|
+
.filter(
|
|
280
|
+
(s) => direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,
|
|
281
|
+
)
|
|
282
|
+
.sort((a, b) => direction * (nearEdge(a, direction) - nearEdge(b, direction)));
|
|
283
|
+
|
|
284
|
+
let bound = edge;
|
|
285
|
+
let budget = padding;
|
|
286
|
+
|
|
287
|
+
for (const section of beyond) {
|
|
288
|
+
const pixelScale = scaleFor(section);
|
|
289
|
+
if (!(pixelScale > 0)) continue;
|
|
290
|
+
|
|
291
|
+
const from =
|
|
292
|
+
direction > 0
|
|
293
|
+
? Math.max(section.t_age, bound)
|
|
294
|
+
: Math.min(section.b_age, bound);
|
|
295
|
+
const available = direction > 0 ? section.b_age - from : from - section.t_age;
|
|
296
|
+
if (available <= 0) continue;
|
|
297
|
+
|
|
298
|
+
const cost = available * pixelScale;
|
|
299
|
+
if (budget <= cost) return from + direction * (budget / pixelScale);
|
|
300
|
+
|
|
301
|
+
bound = direction > 0 ? section.b_age : section.t_age;
|
|
302
|
+
budget -= cost;
|
|
303
|
+
}
|
|
304
|
+
return bound;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** The age at which a section starts, measured from the window edge outward. */
|
|
308
|
+
function nearEdge<T extends UnitLong>(
|
|
309
|
+
section: SectionInfoWithScale<T>,
|
|
310
|
+
direction: 1 | -1,
|
|
311
|
+
): number {
|
|
312
|
+
return direction > 0 ? section.t_age : section.b_age;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Clip laid-out sections to the rendered window — the last step of layout, so
|
|
317
|
+
* that every section has already been sized (and floored) at its full extent.
|
|
318
|
+
* Each section renders at the density `scaleFor` resolved for it, which is what
|
|
319
|
+
* keeps a padding margin the same number of pixels as the band it borders is
|
|
320
|
+
* stretched to meet `minSectionHeight`.
|
|
321
|
+
*/
|
|
322
|
+
export function trimSectionsToWindow<T extends UnitLong>(
|
|
323
|
+
sections: SectionInfoWithScale<T>[],
|
|
324
|
+
window: [number, number],
|
|
325
|
+
scaleFor: (section: SectionInfoWithScale<T>) => number,
|
|
326
|
+
): SectionInfoWithScale<T>[] {
|
|
327
|
+
const [b_win, t_win] = window;
|
|
328
|
+
const out: SectionInfoWithScale<T>[] = [];
|
|
329
|
+
|
|
330
|
+
for (const section of sections) {
|
|
331
|
+
const t_age = Math.max(section.t_age, t_win);
|
|
332
|
+
const b_age = Math.min(section.b_age, b_win);
|
|
333
|
+
if (b_age <= t_age) continue;
|
|
334
|
+
|
|
335
|
+
const pixelScale = scaleFor(section);
|
|
336
|
+
if (
|
|
337
|
+
b_age === section.b_age &&
|
|
338
|
+
t_age === section.t_age &&
|
|
339
|
+
pixelScale === section.scaleInfo.pixelScale
|
|
340
|
+
) {
|
|
341
|
+
out.push(section);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
out.push({
|
|
346
|
+
...section,
|
|
347
|
+
t_age,
|
|
348
|
+
b_age,
|
|
349
|
+
units: section.units.filter((u) => agesOverlap(u, { t_age, b_age })),
|
|
350
|
+
scaleInfo: createPackageScale({
|
|
351
|
+
domain: [b_age, t_age],
|
|
352
|
+
pixelScale,
|
|
353
|
+
pixelHeight: (b_age - t_age) * pixelScale,
|
|
354
|
+
scale: null,
|
|
355
|
+
}),
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return out;
|
|
359
|
+
}
|
|
360
|
+
|
|
189
361
|
export function createPackageScale(
|
|
190
362
|
def: PackageScaleInfo,
|
|
191
363
|
offset: number = 0,
|
|
@@ -260,12 +432,40 @@ function findSectionHeightRange(
|
|
|
260
432
|
function findAverageUnitHeight(
|
|
261
433
|
data: UnitLong[],
|
|
262
434
|
axisType: ColumnAxisType,
|
|
435
|
+
visibleWindow?: [number, number] | null,
|
|
263
436
|
): number {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
437
|
+
/** The typical duration of a unit, which `targetUnitHeight` sizes.
|
|
438
|
+
*
|
|
439
|
+
* Measured over what the render window actually *shows* — units outside it
|
|
440
|
+
* are ignored and a unit it cuts through counts only for its visible part.
|
|
441
|
+
* That's what makes `targetUnitHeight` mean the same thing at every zoom
|
|
442
|
+
* depth: the units on screen are drawn at the target height whether they're
|
|
443
|
+
* whole or slivers of something much longer.
|
|
444
|
+
*
|
|
445
|
+
* A section the window doesn't reach has nothing visible to measure, so it
|
|
446
|
+
* falls back to its own units at full duration — it's drawn at its own scale.
|
|
447
|
+
* (Deriving it from the padded window instead would be circular: padding is
|
|
448
|
+
* measured in pixels, which is what we're trying to establish.)
|
|
449
|
+
*/
|
|
450
|
+
const durations = (clip: boolean) =>
|
|
451
|
+
data
|
|
452
|
+
.map((d) => {
|
|
453
|
+
const [b_pos, t_pos] = getUnitHeightRange(d, axisType);
|
|
454
|
+
if (!clip) return Math.abs(b_pos - t_pos);
|
|
455
|
+
const [b_win, t_win] = visibleWindow;
|
|
456
|
+
return Math.max(
|
|
457
|
+
0,
|
|
458
|
+
Math.min(b_pos, b_win) - Math.max(t_pos, t_win),
|
|
459
|
+
);
|
|
460
|
+
})
|
|
461
|
+
.filter((d) => d > 0);
|
|
462
|
+
|
|
463
|
+
const useWindow = visibleWindow != null && axisType === ColumnAxisType.AGE;
|
|
464
|
+
let heights = useWindow ? durations(true) : [];
|
|
465
|
+
if (heights.length === 0) heights = durations(false);
|
|
466
|
+
if (heights.length === 0) return 1;
|
|
467
|
+
|
|
468
|
+
return heights.reduce((a, b) => a + b, 0) / heights.length;
|
|
269
469
|
}
|
|
270
470
|
|
|
271
471
|
export interface CompositeColumnScale {
|
|
@@ -69,8 +69,6 @@ function groupUnitsByMutualOverlap<T extends BaseUnit>(
|
|
|
69
69
|
|
|
70
70
|
const sections = groupUnitsIntoSectionsByOverlap(units, axisType, tolerance);
|
|
71
71
|
|
|
72
|
-
console.log("Grouped sections", sections);
|
|
73
|
-
|
|
74
72
|
return sections.map((d) => {
|
|
75
73
|
return new UnitGroup(d.units, axisType);
|
|
76
74
|
});
|
|
@@ -15,6 +15,9 @@ import {
|
|
|
15
15
|
collapseUnconformitiesByPixelHeight,
|
|
16
16
|
computeSectionHeights,
|
|
17
17
|
finalizeSectionHeights,
|
|
18
|
+
padWindowByPixels,
|
|
19
|
+
resolveWindowScales,
|
|
20
|
+
trimSectionsToWindow,
|
|
18
21
|
type CompositeColumnScale,
|
|
19
22
|
} from "./composite-scale";
|
|
20
23
|
import {
|
|
@@ -58,6 +61,7 @@ export function prepareColumnUnits(
|
|
|
58
61
|
collapseSmallUnconformities = false,
|
|
59
62
|
hybridScale,
|
|
60
63
|
scale,
|
|
64
|
+
windowPadding = 0,
|
|
61
65
|
} = options;
|
|
62
66
|
|
|
63
67
|
let _totalHeight: number | null = null;
|
|
@@ -76,19 +80,29 @@ export function prepareColumnUnits(
|
|
|
76
80
|
}
|
|
77
81
|
}
|
|
78
82
|
|
|
83
|
+
/** Age columns lay every section out at its full extent and clip to the
|
|
84
|
+
* window at the end (see below), so that a section abutting the window is
|
|
85
|
+
* sized by its own content rather than by the sliver that survives the clip.
|
|
86
|
+
* Hybrid and externally-supplied scales build their own mapping and can't be
|
|
87
|
+
* re-derived that way, so they keep the original clip-then-lay-out path. */
|
|
88
|
+
const clipBeforeLayout =
|
|
89
|
+
axisType != ColumnAxisType.AGE || hybridScale != null || scale != null;
|
|
90
|
+
|
|
79
91
|
// Start by ensuring that ages and positions are numbers
|
|
80
92
|
// also set up some values for eODP-style columns
|
|
81
93
|
let units1 = units.map(preprocessSectionUnit);
|
|
82
94
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
95
|
+
if (clipBeforeLayout) {
|
|
96
|
+
/** Prototype filtering to age range */
|
|
97
|
+
units1 = units1.filter((d) => {
|
|
98
|
+
// Filter units by t_age and b_age, inclusive
|
|
99
|
+
if (axisType == ColumnAxisType.AGE) {
|
|
100
|
+
return agesOverlap(d, { t_age, b_age });
|
|
101
|
+
} else {
|
|
102
|
+
return unitsOverlap(d, { t_pos, b_pos } as any, axisType);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
}
|
|
92
106
|
|
|
93
107
|
let mergeMode = mergeSections;
|
|
94
108
|
// if (axisType != ColumnAxisType.AGE) {
|
|
@@ -123,7 +137,7 @@ export function prepareColumnUnits(
|
|
|
123
137
|
}
|
|
124
138
|
|
|
125
139
|
// Limit sections to the range specified by t_age/b_age or t_pos/b_pos global options
|
|
126
|
-
for (let section of sections0) {
|
|
140
|
+
for (let section of clipBeforeLayout ? sections0 : []) {
|
|
127
141
|
if (axisType == ColumnAxisType.AGE) {
|
|
128
142
|
section.t_age = Math.max(section.t_age, t_age ?? -Infinity);
|
|
129
143
|
section.b_age = Math.min(section.b_age, b_age ?? Infinity);
|
|
@@ -152,11 +166,21 @@ export function prepareColumnUnits(
|
|
|
152
166
|
|
|
153
167
|
// SCALES
|
|
154
168
|
|
|
169
|
+
/** The window we've been asked to render. Unit density is derived from what
|
|
170
|
+
* it shows (see `findAverageUnitHeight`), so it has to be known before any
|
|
171
|
+
* section is laid out — but nothing is *clipped* to it until the very end.
|
|
172
|
+
*/
|
|
173
|
+
const focalWindow: [number, number] | null = clipBeforeLayout
|
|
174
|
+
? null
|
|
175
|
+
: [b_age ?? Infinity, t_age ?? -Infinity];
|
|
176
|
+
|
|
177
|
+
const layoutOptions = { ...options, visibleWindow: focalWindow };
|
|
178
|
+
|
|
155
179
|
/* Compute pixel scales etc. for sections
|
|
156
180
|
* We need to do this now to determine which unconformities
|
|
157
181
|
* are small enough to collapse.
|
|
158
182
|
*/
|
|
159
|
-
let sectionsWithScales = computeSectionHeights(sections,
|
|
183
|
+
let sectionsWithScales = computeSectionHeights(sections, layoutOptions);
|
|
160
184
|
|
|
161
185
|
if (collapseSmallUnconformities && hybridScale == null) {
|
|
162
186
|
// Collapse small unconformities in pixel height space
|
|
@@ -169,7 +193,39 @@ export function prepareColumnUnits(
|
|
|
169
193
|
sectionsWithScales = collapseUnconformitiesByPixelHeight(
|
|
170
194
|
sectionsWithScales,
|
|
171
195
|
threshold,
|
|
172
|
-
|
|
196
|
+
layoutOptions,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Now that every section is laid out at full extent — with its own density
|
|
201
|
+
* and `minSectionHeight` already settled — apply the rendered window. Padding
|
|
202
|
+
* is spent here, against real pixel heights, so `windowPadding` px of an
|
|
203
|
+
* abutting section is exactly that many pixels.
|
|
204
|
+
*/
|
|
205
|
+
if (focalWindow != null && (t_age != null || b_age != null)) {
|
|
206
|
+
// Resolve final densities first (a section the window cuts short is
|
|
207
|
+
// stretched to `minSectionHeight`), then spend the padding budget against
|
|
208
|
+
// them, so a margin is the pixels asked for rather than those pixels times
|
|
209
|
+
// whatever stretch its neighbor happened to need.
|
|
210
|
+
const floor = options.minSectionHeight ?? options.targetUnitHeight ?? 0;
|
|
211
|
+
const scales = resolveWindowScales(sectionsWithScales, focalWindow, floor);
|
|
212
|
+
const scaleFor = (section) =>
|
|
213
|
+
scales.get(section) ?? section.scaleInfo.pixelScale;
|
|
214
|
+
|
|
215
|
+
const window =
|
|
216
|
+
windowPadding > 0
|
|
217
|
+
? padWindowByPixels(
|
|
218
|
+
sectionsWithScales,
|
|
219
|
+
focalWindow,
|
|
220
|
+
windowPadding,
|
|
221
|
+
scaleFor,
|
|
222
|
+
)
|
|
223
|
+
: focalWindow;
|
|
224
|
+
|
|
225
|
+
sectionsWithScales = trimSectionsToWindow(
|
|
226
|
+
sectionsWithScales,
|
|
227
|
+
window,
|
|
228
|
+
scaleFor,
|
|
173
229
|
);
|
|
174
230
|
}
|
|
175
231
|
|
|
@@ -185,8 +241,6 @@ export function prepareColumnUnits(
|
|
|
185
241
|
* We do this after merging sections so that we can
|
|
186
242
|
* handle cases where there are overlapping units across sections
|
|
187
243
|
* */
|
|
188
|
-
console.log(sections2, "sections");
|
|
189
|
-
|
|
190
244
|
const sectionsOut = sections2.map((section) => {
|
|
191
245
|
return {
|
|
192
246
|
...section,
|
|
@@ -14,6 +14,19 @@ export interface ColumnHeightScaleOptions {
|
|
|
14
14
|
* needed because small sections (<1-2 units) don't necessarily have space to comfortably
|
|
15
15
|
* render two axis labels */
|
|
16
16
|
minPixelScale?: number;
|
|
17
|
+
/** The requested render window, `[b_age, t_age]`, if there is one. Set
|
|
18
|
+
* internally by `prepareColumnUnits`: unit density is derived from the units
|
|
19
|
+
* this window actually shows, so `targetUnitHeight` describes the units you
|
|
20
|
+
* can see at any zoom depth rather than the section's overall average. */
|
|
21
|
+
visibleWindow?: [number, number];
|
|
22
|
+
/** Padding around the `t_age`/`b_age` window, in **pixels** of neighboring
|
|
23
|
+
* column: how much of the abutting sections to reveal past the window.
|
|
24
|
+
*
|
|
25
|
+
* Resolved after every section is laid out at full extent, so it's exact —
|
|
26
|
+
* each section's own density and each crossed unconformity's fixed height are
|
|
27
|
+
* spent from the pixel budget. Padding collapses where the column runs out.
|
|
28
|
+
* Ignored for hybrid or externally-supplied scales. */
|
|
29
|
+
windowPadding?: number;
|
|
17
30
|
// Axis scale type
|
|
18
31
|
axisType?: ColumnAxisType;
|
|
19
32
|
// Unconformity height in pixels
|
package/src/section.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
TimescaleOrientation,
|
|
10
10
|
TimescaleClickHandler,
|
|
11
11
|
useMacrostratIntervals,
|
|
12
|
+
type IntervalStyleBuilder,
|
|
12
13
|
} from "@macrostrat/timescale";
|
|
13
14
|
import { ColumnAxisType, SVG } from "@macrostrat/column-components";
|
|
14
15
|
import hyper from "@macrostrat/hyper";
|
|
@@ -203,6 +204,9 @@ function SectionUnits(props: SectionProps) {
|
|
|
203
204
|
interface CompositeTimescaleProps {
|
|
204
205
|
levels?: [number, number] | number;
|
|
205
206
|
unconformityLabels?: boolean;
|
|
207
|
+
onClickInterval?: TimescaleClickHandler;
|
|
208
|
+
/** Per-interval style (e.g. to highlight the currently selected interval). */
|
|
209
|
+
intervalStyle?: IntervalStyleBuilder;
|
|
206
210
|
}
|
|
207
211
|
|
|
208
212
|
export function CompositeTimescale(props: CompositeTimescaleProps) {
|
|
@@ -228,6 +232,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
|
|
|
228
232
|
packages,
|
|
229
233
|
unconformityLabels = false,
|
|
230
234
|
onClickInterval,
|
|
235
|
+
intervalStyle,
|
|
231
236
|
} = props;
|
|
232
237
|
|
|
233
238
|
// Use intervals from Macrostrat API
|
|
@@ -262,6 +267,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
|
|
|
262
267
|
scale,
|
|
263
268
|
intervals,
|
|
264
269
|
onClick: onClickInterval,
|
|
270
|
+
intervalStyle,
|
|
265
271
|
}),
|
|
266
272
|
],
|
|
267
273
|
);
|
package/src/units/boxes.ts
CHANGED
|
@@ -2,15 +2,15 @@ import {
|
|
|
2
2
|
ColumnAxisType,
|
|
3
3
|
ColumnContext,
|
|
4
4
|
ColumnLayoutContext,
|
|
5
|
+
ClippableRect,
|
|
5
6
|
ForeignObject,
|
|
6
7
|
PatternDefsProvider,
|
|
7
8
|
useColumn,
|
|
8
9
|
useGeologicPattern,
|
|
9
|
-
zigZagBoxPath,
|
|
10
10
|
} from "@macrostrat/column-components";
|
|
11
11
|
import { SizeAwareLabel, Clickable } from "@macrostrat/ui-components";
|
|
12
12
|
import hyper from "@macrostrat/hyper";
|
|
13
|
-
import {
|
|
13
|
+
import { ReactNode, useContext, useMemo } from "react";
|
|
14
14
|
import { resolveID, scalePattern } from "./resolvers";
|
|
15
15
|
import { useUnitSelectionTarget } from "../data-provider";
|
|
16
16
|
import { useLithologies } from "@macrostrat/data-provider";
|
|
@@ -23,6 +23,12 @@ import type { RectBounds } from "./types";
|
|
|
23
23
|
|
|
24
24
|
const h = hyper.styled(styles);
|
|
25
25
|
|
|
26
|
+
/** Resolve a class name through this file's CSS module exactly as the styled
|
|
27
|
+
* `h` would (module-scoped classes are hashed; unknown/global classes pass
|
|
28
|
+
* through). Needed because `ClippableRect` renders the element in
|
|
29
|
+
* `@macrostrat/column-components`, so class resolution must happen here. */
|
|
30
|
+
const cls = (name: string): string => (styles as any)[name] ?? name;
|
|
31
|
+
|
|
26
32
|
interface UnitRectOptions {
|
|
27
33
|
widthFraction?: number;
|
|
28
34
|
axisType?: ColumnAxisType;
|
|
@@ -170,70 +176,34 @@ function Unit(props: UnitProps) {
|
|
|
170
176
|
},
|
|
171
177
|
},
|
|
172
178
|
[
|
|
173
|
-
h(
|
|
179
|
+
h(ClippableRect, {
|
|
174
180
|
...bounds,
|
|
175
181
|
fill: backgroundColor,
|
|
176
182
|
onClick,
|
|
177
|
-
className: "background",
|
|
183
|
+
className: cls("background"),
|
|
178
184
|
}),
|
|
179
|
-
h(
|
|
185
|
+
h(ClippableRect, {
|
|
180
186
|
ref,
|
|
181
187
|
...bounds,
|
|
182
188
|
fill: _fill,
|
|
183
189
|
//mask,
|
|
184
190
|
onClick,
|
|
185
|
-
className: "unit",
|
|
191
|
+
className: cls("unit"),
|
|
192
|
+
}),
|
|
193
|
+
h.if(linked)(ClippableRect, {
|
|
194
|
+
...bounds,
|
|
195
|
+
className: cls("linked-overlay"),
|
|
196
|
+
}),
|
|
197
|
+
h.if(selected)(ClippableRect, {
|
|
198
|
+
...bounds,
|
|
199
|
+
className: cls("selection-overlay"),
|
|
186
200
|
}),
|
|
187
|
-
h.if(linked)(UnitRect, { ...bounds, className: "linked-overlay" }),
|
|
188
|
-
h.if(selected)(UnitRect, { ...bounds, className: "selection-overlay" }),
|
|
189
201
|
//defs,
|
|
190
202
|
children,
|
|
191
203
|
],
|
|
192
204
|
);
|
|
193
205
|
}
|
|
194
206
|
|
|
195
|
-
interface UnitRectProps {
|
|
196
|
-
x: number;
|
|
197
|
-
y: number;
|
|
198
|
-
width: number;
|
|
199
|
-
height: number;
|
|
200
|
-
className: string;
|
|
201
|
-
// Used to determine if the rect should have a zig-zag edge on the top or bottom
|
|
202
|
-
overflowTop?: boolean;
|
|
203
|
-
overflowBottom?: boolean;
|
|
204
|
-
[key: string]: any;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const UnitRect = forwardRef((props: UnitRectProps, ref) => {
|
|
208
|
-
const {
|
|
209
|
-
x,
|
|
210
|
-
y,
|
|
211
|
-
width,
|
|
212
|
-
height,
|
|
213
|
-
overflowTop = false,
|
|
214
|
-
overflowBottom = false,
|
|
215
|
-
...rest
|
|
216
|
-
} = props;
|
|
217
|
-
|
|
218
|
-
if (!overflowTop && !overflowBottom) {
|
|
219
|
-
return h("rect", {
|
|
220
|
-
x,
|
|
221
|
-
y,
|
|
222
|
-
width,
|
|
223
|
-
height,
|
|
224
|
-
ref,
|
|
225
|
-
...rest,
|
|
226
|
-
});
|
|
227
|
-
} else {
|
|
228
|
-
const d = zigZagBoxPath(x, y, width, height, overflowTop, overflowBottom);
|
|
229
|
-
return h("path", {
|
|
230
|
-
d,
|
|
231
|
-
ref,
|
|
232
|
-
...rest,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
});
|
|
236
|
-
|
|
237
207
|
function LabeledUnit(props: LabeledUnitProps) {
|
|
238
208
|
const {
|
|
239
209
|
division,
|
|
@@ -247,12 +217,17 @@ function LabeledUnit(props: LabeledUnitProps) {
|
|
|
247
217
|
...baseBounds
|
|
248
218
|
} = props;
|
|
249
219
|
|
|
250
|
-
const { axisType } =
|
|
220
|
+
const { axisType, isTransitioning, hideLabelsWhileTransitioning } =
|
|
221
|
+
useColumn();
|
|
251
222
|
const bounds = {
|
|
252
223
|
...useUnitRect(division, { widthFraction, axisType }),
|
|
253
224
|
...baseBounds,
|
|
254
225
|
};
|
|
255
226
|
const { width, height } = bounds;
|
|
227
|
+
// Labels stay visible through the animation by default. `hideLabelsWhileTransitioning`
|
|
228
|
+
// is a perf escape hatch: when set, the `foreignObject` label (whose HTML
|
|
229
|
+
// reflows on every size change) is skipped mid-transition and restored on settle.
|
|
230
|
+
const skipLabel = isTransitioning && hideLabelsWhileTransitioning;
|
|
256
231
|
return h(
|
|
257
232
|
Unit,
|
|
258
233
|
{
|
|
@@ -263,7 +238,7 @@ function LabeledUnit(props: LabeledUnitProps) {
|
|
|
263
238
|
...bounds,
|
|
264
239
|
},
|
|
265
240
|
[
|
|
266
|
-
h.if(showLabel)(
|
|
241
|
+
h.if(showLabel && !skipLabel)(
|
|
267
242
|
ForeignObject,
|
|
268
243
|
{ ...bounds, className: "unit-label-container" },
|
|
269
244
|
h(SizeAwareLabel, {
|
|
@@ -271,6 +246,9 @@ function LabeledUnit(props: LabeledUnitProps) {
|
|
|
271
246
|
labelClassName: "unit-label",
|
|
272
247
|
style: { width, height },
|
|
273
248
|
label,
|
|
249
|
+
// Re-fit the label when a transition settles (isTransitioning flips),
|
|
250
|
+
// not on every intermediate frame.
|
|
251
|
+
remeasureKey: isTransitioning,
|
|
274
252
|
onVisibilityChanged(viz) {
|
|
275
253
|
onLabelUpdated(label, viz);
|
|
276
254
|
},
|
package/src/units/composite.ts
CHANGED
|
@@ -42,23 +42,32 @@ export function LabelTrackerProvider(props) {
|
|
|
42
42
|
BaseUnit[] | null
|
|
43
43
|
>(null);
|
|
44
44
|
const labelTrackerRef = useRef<LabelTracker>({});
|
|
45
|
+
|
|
46
|
+
const recompute = useCallback(() => {
|
|
47
|
+
// Recompute over the *current* unit set (which changes as the column zooms).
|
|
48
|
+
// The tracker ref accumulates entries for units seen at any zoom level, so
|
|
49
|
+
// we gate on whether every currently-visible unit has reported — not on the
|
|
50
|
+
// total number of tracked units, which would go stale and freeze the notes.
|
|
51
|
+
const tracker = labelTrackerRef.current;
|
|
52
|
+
const allReported = units.every((d) => findUnitID(d) in tracker);
|
|
53
|
+
if (!allReported) return;
|
|
54
|
+
setUnlabeledDivisions(units.filter((d) => tracker[findUnitID(d)] === false));
|
|
55
|
+
}, [units, findUnitID]);
|
|
56
|
+
|
|
45
57
|
const trackLabelVisibility = useCallback(
|
|
46
58
|
(div, visible) => {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (Object.keys(labelTrackerRef.current).length == units.length) {
|
|
50
|
-
setUnlabeledDivisions(
|
|
51
|
-
// @ts-ignore
|
|
52
|
-
units.filter((d) => {
|
|
53
|
-
const id = findUnitID(d);
|
|
54
|
-
return labelTrackerRef.current[id] == false;
|
|
55
|
-
}),
|
|
56
|
-
);
|
|
57
|
-
}
|
|
59
|
+
labelTrackerRef.current[findUnitID(div)] = visible;
|
|
60
|
+
recompute();
|
|
58
61
|
},
|
|
59
|
-
[
|
|
62
|
+
[recompute, findUnitID],
|
|
60
63
|
);
|
|
61
64
|
|
|
65
|
+
// Re-sync when the visible unit set changes (e.g. on zoom), so a unit whose
|
|
66
|
+
// label now fits inline stops also rendering as a note, and vice versa.
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
recompute();
|
|
69
|
+
}, [recompute]);
|
|
70
|
+
|
|
62
71
|
const value = trackLabelVisibility;
|
|
63
72
|
return h(
|
|
64
73
|
LabelTrackerContext.Provider,
|