@macrostrat/column-views 3.11.0 → 3.12.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 +169 -0
- package/dist/age-model/data.cjs +57 -0
- package/dist/age-model/data.cjs.map +1 -0
- package/dist/age-model/data.d.ts +29 -0
- package/dist/age-model/data.js +57 -0
- package/dist/age-model/data.js.map +1 -0
- package/dist/age-model/details.cjs +261 -0
- package/dist/age-model/details.cjs.map +1 -0
- package/dist/age-model/details.d.ts +39 -0
- package/dist/age-model/details.js +258 -0
- package/dist/age-model/details.js.map +1 -0
- package/dist/age-model/details.module.sass.cjs +42 -0
- package/dist/age-model/details.module.sass.cjs.map +1 -0
- package/dist/age-model/details.module.sass.js +40 -0
- package/dist/age-model/details.module.sass.js.map +1 -0
- package/dist/age-model/index.d.ts +5 -0
- package/dist/age-model/legacy.cjs +28 -0
- package/dist/age-model/legacy.cjs.map +1 -0
- package/dist/age-model/legacy.d.ts +6 -0
- package/dist/age-model/legacy.js +26 -0
- package/dist/age-model/legacy.js.map +1 -0
- package/dist/age-model/surfaces.cjs +355 -0
- package/dist/age-model/surfaces.cjs.map +1 -0
- package/dist/age-model/surfaces.d.ts +68 -0
- package/dist/age-model/surfaces.js +352 -0
- package/dist/age-model/surfaces.js.map +1 -0
- package/dist/age-model/surfaces.module.sass.cjs +45 -0
- package/dist/age-model/surfaces.module.sass.cjs.map +1 -0
- package/dist/age-model/surfaces.module.sass.js +43 -0
- package/dist/age-model/surfaces.module.sass.js.map +1 -0
- package/dist/age-model/types.cjs +229 -0
- package/dist/age-model/types.cjs.map +1 -0
- package/dist/age-model/types.d.ts +112 -0
- package/dist/age-model/types.js +229 -0
- package/dist/age-model/types.js.map +1 -0
- package/dist/column-views.css +389 -57
- package/dist/column.cjs +20 -13
- package/dist/column.cjs.map +1 -1
- package/dist/column.d.ts +21 -6
- package/dist/column.js +21 -14
- package/dist/column.js.map +1 -1
- package/dist/column.module.sass.cjs +19 -15
- package/dist/column.module.sass.cjs.map +1 -1
- package/dist/column.module.sass.js +19 -15
- package/dist/column.module.sass.js.map +1 -1
- package/dist/data-provider/store.cjs +34 -1
- package/dist/data-provider/store.cjs.map +1 -1
- package/dist/data-provider/store.d.ts +5 -0
- package/dist/data-provider/store.js +35 -2
- package/dist/data-provider/store.js.map +1 -1
- package/dist/index.cjs +54 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +54 -3
- package/dist/index.js.map +1 -1
- package/dist/notes.cjs +11 -2
- package/dist/notes.cjs.map +1 -1
- package/dist/notes.d.ts +11 -1
- package/dist/notes.js +11 -2
- package/dist/notes.js.map +1 -1
- package/dist/prepare-units/composite-scale.cjs +18 -16
- package/dist/prepare-units/composite-scale.cjs.map +1 -1
- package/dist/prepare-units/composite-scale.js +18 -16
- package/dist/prepare-units/composite-scale.js.map +1 -1
- package/dist/prepare-units/density.cjs +83 -0
- package/dist/prepare-units/density.cjs.map +1 -0
- package/dist/prepare-units/density.d.ts +92 -0
- package/dist/prepare-units/density.js +83 -0
- package/dist/prepare-units/density.js.map +1 -0
- package/dist/prepare-units/index.cjs +15 -7
- package/dist/prepare-units/index.cjs.map +1 -1
- package/dist/prepare-units/index.d.ts +1 -0
- package/dist/prepare-units/index.js +15 -7
- 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 +34 -13
- package/dist/prepare-units/types.js.map +1 -1
- package/dist/section.cjs +108 -24
- package/dist/section.cjs.map +1 -1
- package/dist/section.d.ts +66 -6
- package/dist/section.js +109 -25
- package/dist/section.js.map +1 -1
- package/dist/timescale-zoom.cjs +191 -0
- package/dist/timescale-zoom.cjs.map +1 -0
- package/dist/timescale-zoom.d.ts +74 -0
- package/dist/timescale-zoom.js +191 -0
- package/dist/timescale-zoom.js.map +1 -0
- package/dist/unit-details/age-range.cjs +1 -0
- package/dist/unit-details/age-range.cjs.map +1 -1
- package/dist/unit-details/age-range.d.ts +5 -1
- package/dist/unit-details/age-range.js +1 -0
- package/dist/unit-details/age-range.js.map +1 -1
- package/dist/units/composite.cjs +4 -1
- package/dist/units/composite.cjs.map +1 -1
- package/dist/units/composite.d.ts +2 -0
- package/dist/units/composite.js +4 -1
- package/dist/units/composite.js.map +1 -1
- package/package.json +7 -7
- package/src/age-model/data.ts +98 -0
- package/src/age-model/details.module.sass +108 -0
- package/src/age-model/details.ts +333 -0
- package/src/age-model/index.ts +5 -0
- package/src/age-model/legacy.ts +30 -0
- package/src/age-model/surfaces.module.sass +168 -0
- package/src/age-model/surfaces.ts +568 -0
- package/src/age-model/types.ts +392 -0
- package/src/column.module.sass +59 -8
- package/src/column.ts +49 -23
- package/src/data-provider/store.ts +50 -1
- package/src/index.ts +5 -1
- package/src/notes.ts +20 -2
- package/src/prepare-units/composite-scale.ts +34 -31
- package/src/prepare-units/density.ts +197 -0
- package/src/prepare-units/index.ts +25 -6
- package/src/prepare-units/types.ts +34 -13
- package/src/section.ts +219 -33
- package/src/timescale-zoom.ts +341 -0
- package/src/unit-details/age-range.ts +3 -3
- package/src/units/composite.ts +11 -0
- package/dist/age-model-overlay.cjs +0 -50
- package/dist/age-model-overlay.cjs.map +0 -1
- package/dist/age-model-overlay.d.ts +0 -24
- package/dist/age-model-overlay.js +0 -48
- package/dist/age-model-overlay.js.map +0 -1
- package/dist/age-model-overlay.module.sass.cjs +0 -12
- package/dist/age-model-overlay.module.sass.cjs.map +0 -1
- package/dist/age-model-overlay.module.sass.js +0 -11
- package/dist/age-model-overlay.module.sass.js.map +0 -1
- package/src/age-model-overlay.module.sass +0 -11
- package/src/age-model-overlay.ts +0 -96
package/src/section.ts
CHANGED
|
@@ -3,14 +3,23 @@ import {
|
|
|
3
3
|
LabelTrackerProvider,
|
|
4
4
|
SectionLabelsColumn,
|
|
5
5
|
} from "./units";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type CSSProperties,
|
|
8
|
+
ReactNode,
|
|
9
|
+
FunctionComponent,
|
|
10
|
+
useMemo,
|
|
11
|
+
} from "react";
|
|
7
12
|
import {
|
|
8
13
|
Timescale,
|
|
9
14
|
TimescaleOrientation,
|
|
10
|
-
|
|
11
|
-
|
|
15
|
+
useMacrostratTimescales,
|
|
16
|
+
type Interval,
|
|
12
17
|
type IntervalStyleBuilder,
|
|
18
|
+
type TimescaleClickData,
|
|
13
19
|
} from "@macrostrat/timescale";
|
|
20
|
+
|
|
21
|
+
/** Macrostrat's international timescale, the default for a column */
|
|
22
|
+
const INTERNATIONAL_TIMESCALE_ID = 11;
|
|
14
23
|
import { ColumnAxisType, SVG } from "@macrostrat/column-components";
|
|
15
24
|
import hyper from "@macrostrat/hyper";
|
|
16
25
|
import styles from "./column.module.sass";
|
|
@@ -19,10 +28,6 @@ import {
|
|
|
19
28
|
useMacrostratUnits,
|
|
20
29
|
MacrostratColumnProvider,
|
|
21
30
|
} from "./data-provider";
|
|
22
|
-
import {
|
|
23
|
-
useMacrostratBaseURL,
|
|
24
|
-
useMacrostratData,
|
|
25
|
-
} from "@macrostrat/data-provider";
|
|
26
31
|
import { Duration } from "./unit-details";
|
|
27
32
|
import { Value } from "@macrostrat/data-components";
|
|
28
33
|
import type { ExtUnit, PackageScaleLayoutData } from "./prepare-units/types";
|
|
@@ -37,6 +42,8 @@ export interface SectionSharedProps {
|
|
|
37
42
|
columnWidth?: number;
|
|
38
43
|
children?: ReactNode;
|
|
39
44
|
showLabelColumn?: boolean;
|
|
45
|
+
/** Suppress the label for a unit drawn thinner than this many pixels */
|
|
46
|
+
labelSuppressHeight?: number;
|
|
40
47
|
axisType?: ColumnAxisType;
|
|
41
48
|
className?: string;
|
|
42
49
|
clipUnits?: boolean;
|
|
@@ -63,6 +70,7 @@ export function SectionsColumn(props: SectionSharedProps) {
|
|
|
63
70
|
showLabelColumn = true,
|
|
64
71
|
clipUnits = true,
|
|
65
72
|
maxInternalColumns,
|
|
73
|
+
labelSuppressHeight,
|
|
66
74
|
} = props;
|
|
67
75
|
|
|
68
76
|
const units = useMacrostratUnits();
|
|
@@ -82,6 +90,7 @@ export function SectionsColumn(props: SectionSharedProps) {
|
|
|
82
90
|
}),
|
|
83
91
|
h.if(showLabelColumn)(SectionLabelsColumn, {
|
|
84
92
|
width: width - columnWidth,
|
|
93
|
+
labelSuppressHeight,
|
|
85
94
|
}),
|
|
86
95
|
]);
|
|
87
96
|
}
|
|
@@ -98,7 +107,8 @@ function SectionUnitsColumn(props: SectionSharedProps) {
|
|
|
98
107
|
unconformityLabels = true,
|
|
99
108
|
} = props;
|
|
100
109
|
|
|
101
|
-
const { sections, totalHeight } =
|
|
110
|
+
const { sections, totalHeight, axisType: columnAxisType } =
|
|
111
|
+
useMacrostratColumnData();
|
|
102
112
|
|
|
103
113
|
const scaleData: PackageScaleLayoutData[] = sections.map((section) => {
|
|
104
114
|
return section.scaleInfo;
|
|
@@ -138,6 +148,9 @@ function SectionUnitsColumn(props: SectionSharedProps) {
|
|
|
138
148
|
h.if(unconformityLabels)(UnconformityLabels, {
|
|
139
149
|
width,
|
|
140
150
|
sections: scaleData,
|
|
151
|
+
// A gap between sections is measured in whatever the axis is: metres
|
|
152
|
+
// down a core, Myr across a time column
|
|
153
|
+
axisType: axisType ?? columnAxisType,
|
|
141
154
|
verbose: false,
|
|
142
155
|
}),
|
|
143
156
|
]);
|
|
@@ -201,12 +214,77 @@ function SectionUnits(props: SectionProps) {
|
|
|
201
214
|
);
|
|
202
215
|
}
|
|
203
216
|
|
|
217
|
+
/** A per-interval style that also knows which timescale the interval was
|
|
218
|
+
* drawn from. The same interval often appears in several of the timescales
|
|
219
|
+
* beside each other, and they aren't interchangeable — only one of them is
|
|
220
|
+
* the one a selection was made in. */
|
|
221
|
+
export type ColumnIntervalStyleBuilder =
|
|
222
|
+
| CSSProperties
|
|
223
|
+
| ((interval: Interval, timescaleID: number) => CSSProperties)
|
|
224
|
+
| null;
|
|
225
|
+
|
|
226
|
+
/** A timescale click, reporting which timescale was clicked in */
|
|
227
|
+
export type ColumnTimescaleClickHandler = (
|
|
228
|
+
event: Event,
|
|
229
|
+
data: TimescaleClickData & { timescaleID: number },
|
|
230
|
+
) => void;
|
|
231
|
+
|
|
232
|
+
/** A timescale to draw beside a column.
|
|
233
|
+
*
|
|
234
|
+
* The international timescale is one of these, not a special case: it is the
|
|
235
|
+
* one that happens to be nested several levels deep. Anything with the same
|
|
236
|
+
* shape can take its place or sit beside it — a regional set from the API, a
|
|
237
|
+
* zonation assembled locally, or a hierarchy that isn't a timescale at all,
|
|
238
|
+
* so long as its "intervals" carry ages.
|
|
239
|
+
*/
|
|
240
|
+
export interface ColumnTimescale {
|
|
241
|
+
/** Integer identifier. Clicks and per-interval styles report it, so a
|
|
242
|
+
* consumer can tell one column's copy of an interval from another's. For a
|
|
243
|
+
* Macrostrat timescale it is the `timescale_id`. */
|
|
244
|
+
id: number;
|
|
245
|
+
/** Shown above the column, unless `label` says otherwise */
|
|
246
|
+
name?: string;
|
|
247
|
+
/** The intervals themselves, flat or already nested by `pid`. Left out,
|
|
248
|
+
* the timescale `id` is fetched from the Macrostrat API. */
|
|
249
|
+
intervals?: Interval[];
|
|
250
|
+
/** Levels to draw, and so how many slots wide the column is (default
|
|
251
|
+
* `[1, 1]`: one level, which is what a flat timescale has) */
|
|
252
|
+
levels?: [number, number];
|
|
253
|
+
/** `oid` of the interval the tree hangs from (default 0) */
|
|
254
|
+
rootInterval?: number;
|
|
255
|
+
/** Drawn above the column in place of the name — any node, so a consumer
|
|
256
|
+
* can put a control or a legend there instead of text */
|
|
257
|
+
label?: ReactNode;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export type ColumnTimescaleLike = number | ColumnTimescale;
|
|
261
|
+
|
|
204
262
|
interface CompositeTimescaleProps {
|
|
263
|
+
/** Every timescale to draw, in order. A bare number is a Macrostrat
|
|
264
|
+
* timescale to fetch and draw as a single level.
|
|
265
|
+
*
|
|
266
|
+
* Given this, `levels`, `timescaleID` and `additionalTimescales` are
|
|
267
|
+
* ignored: it says what they say and more. */
|
|
268
|
+
timescales?: ColumnTimescaleLike[];
|
|
269
|
+
/** Levels of the ICS hierarchy to show. If a number, the finest level is
|
|
270
|
+
* `levels[0]`, the second-finest is `levels[1]`, etc. If an array, the
|
|
271
|
+
* first element is the starting level, the second is the ending level. */
|
|
205
272
|
levels?: [number, number] | number;
|
|
206
273
|
unconformityLabels?: boolean;
|
|
207
|
-
onClickInterval?:
|
|
274
|
+
onClickInterval?: ColumnTimescaleClickHandler;
|
|
208
275
|
/** Per-interval style (e.g. to highlight the currently selected interval). */
|
|
209
|
-
intervalStyle?:
|
|
276
|
+
intervalStyle?: ColumnIntervalStyleBuilder;
|
|
277
|
+
/** The timescale to draw. Defaults to the international one; any other
|
|
278
|
+
* Macrostrat timescale (a regional or project-specific set of intervals)
|
|
279
|
+
* can be drawn in its place. */
|
|
280
|
+
timescaleID?: number;
|
|
281
|
+
/** Further timescales drawn as extra level columns beside the main one,
|
|
282
|
+
* against the same scale. Shorthand for listing them in `timescales`. */
|
|
283
|
+
additionalTimescales?: number[];
|
|
284
|
+
/** Draw the names above the columns. They sit above the column proper, so
|
|
285
|
+
* leave top padding for them. */
|
|
286
|
+
showLabels?: boolean;
|
|
287
|
+
className?: string;
|
|
210
288
|
}
|
|
211
289
|
|
|
212
290
|
export function CompositeTimescale(props: CompositeTimescaleProps) {
|
|
@@ -223,53 +301,85 @@ export function CompositeTimescale(props: CompositeTimescaleProps) {
|
|
|
223
301
|
|
|
224
302
|
type CompositeTimescaleCoreProps = CompositeTimescaleProps & {
|
|
225
303
|
packages: PackageScaleLayoutData[];
|
|
226
|
-
onClickInterval?: TimescaleClickHandler;
|
|
227
304
|
};
|
|
228
305
|
|
|
306
|
+
/** What is drawn above a timescale: its name, reading up the page, or
|
|
307
|
+
* whatever node the consumer gave instead. */
|
|
308
|
+
function TimescaleLabel({ timescale }: { timescale: ColumnTimescale }) {
|
|
309
|
+
const { label, name } = timescale;
|
|
310
|
+
const style = levelCountStyle(levelCount(timescale));
|
|
311
|
+
|
|
312
|
+
let content: ReactNode = label;
|
|
313
|
+
if (label == null && name != null) {
|
|
314
|
+
content = h("span.timescale-label-text", name);
|
|
315
|
+
}
|
|
316
|
+
if (content == null) return h("div.timescale-label", { style });
|
|
317
|
+
|
|
318
|
+
return h("div.timescale-label", { style }, content);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** How wide a timescale draws: one slot per level it shows. */
|
|
322
|
+
function levelCountStyle(levelCount: number): CSSProperties {
|
|
323
|
+
return { "--timescale-level-count": levelCount } as CSSProperties;
|
|
324
|
+
}
|
|
325
|
+
|
|
229
326
|
export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
|
|
230
327
|
const {
|
|
231
|
-
levels = 3,
|
|
232
328
|
packages,
|
|
233
329
|
unconformityLabels = false,
|
|
234
330
|
onClickInterval,
|
|
235
331
|
intervalStyle,
|
|
332
|
+
showLabels = false,
|
|
333
|
+
className,
|
|
236
334
|
} = props;
|
|
237
335
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
336
|
+
const timescales = useColumnTimescales(props);
|
|
337
|
+
|
|
338
|
+
/** Clicks and styles carry the timescale they came from, so a consumer can
|
|
339
|
+
* tell one column's copy of an interval from another's. */
|
|
340
|
+
function forTimescale(id: number) {
|
|
341
|
+
let onClick = null;
|
|
342
|
+
if (onClickInterval != null) {
|
|
343
|
+
onClick = (event: Event, data: TimescaleClickData) => {
|
|
344
|
+
onClickInterval(event, { ...data, timescaleID: id });
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
let style = intervalStyle as IntervalStyleBuilder;
|
|
348
|
+
if (typeof intervalStyle === "function") {
|
|
349
|
+
style = (interval: Interval) => intervalStyle(interval, id);
|
|
350
|
+
}
|
|
351
|
+
return { onClick, intervalStyle: style };
|
|
248
352
|
}
|
|
249
353
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
354
|
+
return h("div.timescale-column", { className }, [
|
|
355
|
+
h.if(showLabels)(
|
|
356
|
+
"div.timescale-labels",
|
|
357
|
+
timescales.map((timescale) =>
|
|
358
|
+
h(TimescaleLabel, { key: timescale.id, timescale }),
|
|
359
|
+
),
|
|
360
|
+
),
|
|
253
361
|
h(
|
|
254
362
|
"div.timescales",
|
|
255
363
|
packages.map((group) => {
|
|
256
364
|
const { pixelHeight, paddingTop, key, scale } = group;
|
|
257
365
|
return h(
|
|
258
|
-
"div.
|
|
259
|
-
{ style: { paddingTop
|
|
260
|
-
|
|
366
|
+
"div.section-timescales",
|
|
367
|
+
{ style: { paddingTop }, key },
|
|
368
|
+
timescales.map((timescale) =>
|
|
261
369
|
h(Timescale, {
|
|
370
|
+
key: timescale.id,
|
|
262
371
|
orientation: TimescaleOrientation.VERTICAL,
|
|
263
372
|
length: pixelHeight,
|
|
264
|
-
levels:
|
|
373
|
+
levels: timescale.levels,
|
|
374
|
+
rootInterval: timescale.rootInterval,
|
|
265
375
|
absoluteAgeScale: true,
|
|
266
376
|
showAgeAxis: false,
|
|
267
377
|
scale,
|
|
268
|
-
intervals,
|
|
269
|
-
|
|
270
|
-
|
|
378
|
+
intervals: timescale.intervals,
|
|
379
|
+
style: levelCountStyle(levelCount(timescale)),
|
|
380
|
+
...forTimescale(timescale.id),
|
|
271
381
|
}),
|
|
272
|
-
|
|
382
|
+
),
|
|
273
383
|
);
|
|
274
384
|
}),
|
|
275
385
|
),
|
|
@@ -282,6 +392,82 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
|
|
|
282
392
|
]);
|
|
283
393
|
}
|
|
284
394
|
|
|
395
|
+
/** The timescales to draw, with their intervals.
|
|
396
|
+
*
|
|
397
|
+
* Every timescale a column draws is fetched in one place: a column is split
|
|
398
|
+
* into sections, and fetching per section would ask for the same timescale
|
|
399
|
+
* once per section per timescale.
|
|
400
|
+
*/
|
|
401
|
+
function useColumnTimescales(props: CompositeTimescaleProps): ColumnTimescale[] {
|
|
402
|
+
const { timescales, levels = 3, timescaleID, additionalTimescales } = props;
|
|
403
|
+
|
|
404
|
+
const specs = useMemo(
|
|
405
|
+
() => timescaleSpecs({ timescales, levels, timescaleID, additionalTimescales }),
|
|
406
|
+
[
|
|
407
|
+
timescales,
|
|
408
|
+
levels,
|
|
409
|
+
timescaleID,
|
|
410
|
+
additionalTimescales?.join(","),
|
|
411
|
+
],
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
// Only the ones that didn't come with their own intervals need fetching
|
|
415
|
+
const fetchIDs = useMemo(
|
|
416
|
+
() => specs.filter((d) => d.intervals == null).map((d) => d.id),
|
|
417
|
+
[specs],
|
|
418
|
+
);
|
|
419
|
+
const intervalSets = useMacrostratTimescales(fetchIDs);
|
|
420
|
+
|
|
421
|
+
return useMemo(
|
|
422
|
+
() =>
|
|
423
|
+
specs.map((spec) => ({
|
|
424
|
+
...spec,
|
|
425
|
+
intervals: spec.intervals ?? intervalSets.get(spec.id) ?? [],
|
|
426
|
+
})),
|
|
427
|
+
[specs, intervalSets],
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** The `timescales` list, or one built from the older props that name the
|
|
432
|
+
* leveled timescale and the flat ones beside it. */
|
|
433
|
+
function timescaleSpecs(props: CompositeTimescaleProps): ColumnTimescale[] {
|
|
434
|
+
const { timescales, levels = 3, timescaleID, additionalTimescales = [] } = props;
|
|
435
|
+
|
|
436
|
+
if (timescales != null) {
|
|
437
|
+
return timescales.map(normalizeTimescale);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return [
|
|
441
|
+
{
|
|
442
|
+
id: timescaleID ?? INTERNATIONAL_TIMESCALE_ID,
|
|
443
|
+
levels: levelRange(levels),
|
|
444
|
+
},
|
|
445
|
+
...additionalTimescales.map(normalizeTimescale),
|
|
446
|
+
];
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeTimescale(timescale: ColumnTimescaleLike): ColumnTimescale {
|
|
450
|
+
if (typeof timescale === "number") {
|
|
451
|
+
return { id: timescale, levels: FLAT_TIMESCALE_LEVELS };
|
|
452
|
+
}
|
|
453
|
+
return { levels: FLAT_TIMESCALE_LEVELS, ...timescale };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/** Regional and project timescales come back flat: one level of intervals. */
|
|
457
|
+
const FLAT_TIMESCALE_LEVELS: [number, number] = [1, 1];
|
|
458
|
+
|
|
459
|
+
function levelRange(levels: [number, number] | number): [number, number] {
|
|
460
|
+
if (typeof levels !== "number") return levels;
|
|
461
|
+
// A count, rather than a range: start at the level most columns start at
|
|
462
|
+
return [2, Math.max(2 + Math.min(levels, 5) - 1, 1)];
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** How many slots wide a timescale draws: one per level it shows. */
|
|
466
|
+
function levelCount(timescale: ColumnTimescale): number {
|
|
467
|
+
const [min, max] = timescale.levels ?? FLAT_TIMESCALE_LEVELS;
|
|
468
|
+
return Math.max(max - min + 1, 1);
|
|
469
|
+
}
|
|
470
|
+
|
|
285
471
|
export function UnconformityLabels(props: {
|
|
286
472
|
width: string | number;
|
|
287
473
|
sections: PackageScaleLayoutData[];
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
/** Click-to-zoom navigation over geologic time.
|
|
2
|
+
*
|
|
3
|
+
* `useTimescaleZoom` turns clicks on a column's timescale into an animated
|
|
4
|
+
* age window: clicking an interval zooms to it, clicking the interval you are
|
|
5
|
+
* already in zooms back out a level, and the timescale shows a sliding window
|
|
6
|
+
* of levels that follows the selection, so finer intervals come into reach as
|
|
7
|
+
* you drill. The result spreads straight onto a `Column`.
|
|
8
|
+
*/
|
|
9
|
+
import { type CSSProperties, useCallback, useMemo, useState } from "react";
|
|
10
|
+
import type {
|
|
11
|
+
Interval,
|
|
12
|
+
IntervalStyleBuilder,
|
|
13
|
+
TimescaleClickData,
|
|
14
|
+
TimescaleClickHandler,
|
|
15
|
+
} from "@macrostrat/timescale";
|
|
16
|
+
import { type AgeWindow, useAnimatedAgeWindow } from "./animated-age-window";
|
|
17
|
+
|
|
18
|
+
/** Coarsest level worth showing; level 0 is "all of geologic time" */
|
|
19
|
+
const MIN_TIMESCALE_LEVEL = 1;
|
|
20
|
+
/** Deepest level in the timescale (age/stage) */
|
|
21
|
+
const MAX_TIMESCALE_LEVEL = 5;
|
|
22
|
+
/** How many levels are shown at once */
|
|
23
|
+
const LEVEL_WINDOW = 3;
|
|
24
|
+
/** The level anchored on before an interval is picked. 3 (period) puts the
|
|
25
|
+
* starting window at era–epoch, the levels a `Column` shows by default. */
|
|
26
|
+
const DEFAULT_LEVEL = 3;
|
|
27
|
+
|
|
28
|
+
const SELECTED_INTERVAL_STYLE: CSSProperties = { fontWeight: "bold" };
|
|
29
|
+
|
|
30
|
+
export interface UseTimescaleZoomOptions {
|
|
31
|
+
/** The full data extent — the window zooming returns to. `null` until the
|
|
32
|
+
* data (and hence the extent) is known. */
|
|
33
|
+
fullExtent: AgeWindow | null;
|
|
34
|
+
/** When false the hook reports no window and no column props, so a view can
|
|
35
|
+
* offer zooming as an option without changing hook order (default `true`). */
|
|
36
|
+
enabled?: boolean;
|
|
37
|
+
/** How many timescale levels to show at once (default 3) */
|
|
38
|
+
levelWindow?: number;
|
|
39
|
+
/** The level to anchor the window on before an interval is picked
|
|
40
|
+
* (default 3, period) */
|
|
41
|
+
defaultLevel?: number;
|
|
42
|
+
minLevel?: number;
|
|
43
|
+
maxLevel?: number;
|
|
44
|
+
/** Animation duration in ms */
|
|
45
|
+
duration?: number;
|
|
46
|
+
/** Style for the selected interval — the one whose click zooms out
|
|
47
|
+
* (default bold) */
|
|
48
|
+
selectedIntervalStyle?: CSSProperties;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Timescale and age-window props, ready to spread onto a `Column`. */
|
|
52
|
+
export interface TimescaleZoomColumnProps {
|
|
53
|
+
showTimescale?: boolean;
|
|
54
|
+
timescaleLevels?: [number, number];
|
|
55
|
+
timescaleIntervalStyle?: IntervalStyleBuilder;
|
|
56
|
+
onClickTimescaleInterval?: TimescaleClickHandler;
|
|
57
|
+
t_age?: number;
|
|
58
|
+
b_age?: number;
|
|
59
|
+
isTransitioning?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface TimescaleZoom {
|
|
63
|
+
enabled: boolean;
|
|
64
|
+
/** The rendered age window — `null` when disabled or before the extent is
|
|
65
|
+
* known */
|
|
66
|
+
window: AgeWindow | null;
|
|
67
|
+
/** The drill path: coarse to fine, the last entry being the selection */
|
|
68
|
+
intervals: Interval[];
|
|
69
|
+
selectedInterval: Interval | null;
|
|
70
|
+
/** The timescale the selection was made in, when the click said so */
|
|
71
|
+
selectedTimescaleID: number | null;
|
|
72
|
+
/** Every interval in the selection: the one drilled to, plus any added by
|
|
73
|
+
* shift-clicking. The window spans all of them. */
|
|
74
|
+
selectedIntervals: Interval[];
|
|
75
|
+
/** The levels the timescale should show, following the selection */
|
|
76
|
+
timescaleLevels: [number, number];
|
|
77
|
+
isFullExtent: boolean;
|
|
78
|
+
isAnimating: boolean;
|
|
79
|
+
/** Return to the full extent, clearing the drill path */
|
|
80
|
+
reset(): void;
|
|
81
|
+
zoomToInterval(interval: Interval): void;
|
|
82
|
+
onClickTimescaleInterval: TimescaleClickHandler;
|
|
83
|
+
timescaleIntervalStyle: (interval: Interval) => CSSProperties;
|
|
84
|
+
columnProps: TimescaleZoomColumnProps;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function useTimescaleZoom(
|
|
88
|
+
options: UseTimescaleZoomOptions,
|
|
89
|
+
): TimescaleZoom {
|
|
90
|
+
const {
|
|
91
|
+
fullExtent,
|
|
92
|
+
enabled = true,
|
|
93
|
+
levelWindow = LEVEL_WINDOW,
|
|
94
|
+
defaultLevel = DEFAULT_LEVEL,
|
|
95
|
+
minLevel = MIN_TIMESCALE_LEVEL,
|
|
96
|
+
maxLevel = MAX_TIMESCALE_LEVEL,
|
|
97
|
+
duration,
|
|
98
|
+
selectedIntervalStyle = SELECTED_INTERVAL_STYLE,
|
|
99
|
+
} = options;
|
|
100
|
+
|
|
101
|
+
const anim = useAnimatedAgeWindow({ fullExtent, duration });
|
|
102
|
+
|
|
103
|
+
// The intervals drilled through; the last one is the current selection
|
|
104
|
+
const [intervals, setIntervals] = useState<Interval[]>([]);
|
|
105
|
+
// Intervals shift-clicked into the selection alongside it
|
|
106
|
+
const [addedIntervals, setAddedIntervals] = useState<Interval[]>([]);
|
|
107
|
+
// Which timescale the selection was made in. The same interval is drawn in
|
|
108
|
+
// every timescale that contains it, and those copies are not the same
|
|
109
|
+
// selection: clicking one in another column moves the selection there.
|
|
110
|
+
const [selectedTimescaleID, setSelectedTimescaleID] = useState<number | null>(
|
|
111
|
+
null,
|
|
112
|
+
);
|
|
113
|
+
const selectedInterval = intervals[intervals.length - 1] ?? null;
|
|
114
|
+
|
|
115
|
+
const selectedIntervals = useMemo(() => {
|
|
116
|
+
if (selectedInterval == null) return addedIntervals;
|
|
117
|
+
return [selectedInterval, ...addedIntervals];
|
|
118
|
+
}, [selectedInterval, addedIntervals]);
|
|
119
|
+
|
|
120
|
+
const reset = useCallback(() => {
|
|
121
|
+
setIntervals([]);
|
|
122
|
+
setAddedIntervals([]);
|
|
123
|
+
setSelectedTimescaleID(null);
|
|
124
|
+
anim.reset();
|
|
125
|
+
}, [anim.reset]);
|
|
126
|
+
|
|
127
|
+
/** Take the interval clicked into (or out of) the selection, and span the
|
|
128
|
+
* result — how you widen a window to the interval next door. */
|
|
129
|
+
const extendToInterval = useCallback(
|
|
130
|
+
(interval: Interval) => {
|
|
131
|
+
let added = addedIntervals.filter((d) => d.oid !== interval.oid);
|
|
132
|
+
const wasSelected = added.length !== addedIntervals.length;
|
|
133
|
+
if (!wasSelected && interval.oid !== selectedInterval?.oid) {
|
|
134
|
+
added = [...addedIntervals, interval];
|
|
135
|
+
}
|
|
136
|
+
setAddedIntervals(added);
|
|
137
|
+
|
|
138
|
+
let selection = added;
|
|
139
|
+
if (selectedInterval != null) {
|
|
140
|
+
selection = [selectedInterval, ...added];
|
|
141
|
+
}
|
|
142
|
+
if (selection.length === 0) {
|
|
143
|
+
anim.reset();
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
anim.zoomToWindow(intervalsWindow(selection));
|
|
147
|
+
},
|
|
148
|
+
[addedIntervals, selectedInterval, anim.zoomToWindow, anim.reset],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const zoomToInterval = useCallback(
|
|
152
|
+
(interval: Interval, timescaleID: number | null = null) => {
|
|
153
|
+
// Keep only the coarser intervals that actually contain this one, so
|
|
154
|
+
// stepping sideways into a different parent (the last stage of the
|
|
155
|
+
// Cambrian → the first of the Ordovician) doesn't strand the old one
|
|
156
|
+
const containing = intervals.filter(
|
|
157
|
+
(d) =>
|
|
158
|
+
d.lvl < interval.lvl &&
|
|
159
|
+
d.eag >= interval.eag &&
|
|
160
|
+
d.lag <= interval.lag,
|
|
161
|
+
);
|
|
162
|
+
setIntervals([...containing, interval]);
|
|
163
|
+
setAddedIntervals([]);
|
|
164
|
+
setSelectedTimescaleID(timescaleID);
|
|
165
|
+
anim.zoomToInterval(interval);
|
|
166
|
+
},
|
|
167
|
+
[intervals, anim.zoomToInterval],
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const onClickTimescaleInterval = useCallback<TimescaleClickHandler>(
|
|
171
|
+
(event: Event, data: TimescaleClickData & { timescaleID?: number }) => {
|
|
172
|
+
const interval = data?.interval;
|
|
173
|
+
if (interval == null || interval.lvl == null) return;
|
|
174
|
+
|
|
175
|
+
const timescaleID = data.timescaleID ?? null;
|
|
176
|
+
// A click in a different timescale is a different selection, even on the
|
|
177
|
+
// same interval: it moves to that column rather than zooming back out
|
|
178
|
+
const sameTimescale =
|
|
179
|
+
timescaleID == null ||
|
|
180
|
+
selectedTimescaleID == null ||
|
|
181
|
+
timescaleID === selectedTimescaleID;
|
|
182
|
+
|
|
183
|
+
// Shift-click widens the window instead of moving it, so a selection can
|
|
184
|
+
// grow into the interval next door
|
|
185
|
+
if ((event as MouseEvent)?.shiftKey) {
|
|
186
|
+
extendToInterval(interval);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Clicking the interval you are in is the way back out: pop a level, or
|
|
191
|
+
// return to the full extent past the root
|
|
192
|
+
if (interval.oid === selectedInterval?.oid && addedIntervals.length === 0) {
|
|
193
|
+
const next = intervals.slice(0, -1);
|
|
194
|
+
setIntervals(next);
|
|
195
|
+
setSelectedTimescaleID(null);
|
|
196
|
+
const parent = next[next.length - 1] ?? null;
|
|
197
|
+
if (parent == null) {
|
|
198
|
+
anim.reset();
|
|
199
|
+
} else {
|
|
200
|
+
anim.zoomToInterval(parent);
|
|
201
|
+
}
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Every other click navigates *to* the interval clicked, whatever its
|
|
206
|
+
// rank: a finer one drills in, a neighbor moves along the timescale, a
|
|
207
|
+
// coarser one zooms out to it
|
|
208
|
+
zoomToInterval(interval, timescaleID);
|
|
209
|
+
},
|
|
210
|
+
[
|
|
211
|
+
intervals,
|
|
212
|
+
selectedInterval,
|
|
213
|
+
selectedTimescaleID,
|
|
214
|
+
addedIntervals,
|
|
215
|
+
extendToInterval,
|
|
216
|
+
zoomToInterval,
|
|
217
|
+
anim.reset,
|
|
218
|
+
],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const timescaleIntervalStyle = useCallback(
|
|
222
|
+
(interval: Interval): CSSProperties => {
|
|
223
|
+
if (selectedIntervals.some((d) => d.oid === interval.oid)) {
|
|
224
|
+
return selectedIntervalStyle;
|
|
225
|
+
}
|
|
226
|
+
return {};
|
|
227
|
+
},
|
|
228
|
+
[selectedIntervals, selectedIntervalStyle],
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
const levels = useMemo(() => {
|
|
232
|
+
let level = defaultLevel;
|
|
233
|
+
for (const interval of selectedIntervals) {
|
|
234
|
+
if (interval.lvl != null) level = Math.max(level, interval.lvl);
|
|
235
|
+
}
|
|
236
|
+
return levelsForSelected(level, { levelWindow, minLevel, maxLevel });
|
|
237
|
+
}, [selectedIntervals, defaultLevel, levelWindow, minLevel, maxLevel]);
|
|
238
|
+
|
|
239
|
+
if (!enabled) {
|
|
240
|
+
return {
|
|
241
|
+
...disabledZoom,
|
|
242
|
+
selectedTimescaleID: null,
|
|
243
|
+
selectedIntervals: [],
|
|
244
|
+
timescaleLevels: levels,
|
|
245
|
+
reset,
|
|
246
|
+
zoomToInterval,
|
|
247
|
+
onClickTimescaleInterval,
|
|
248
|
+
timescaleIntervalStyle,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const window = anim.window ?? fullExtent;
|
|
253
|
+
|
|
254
|
+
return {
|
|
255
|
+
enabled: true,
|
|
256
|
+
window,
|
|
257
|
+
intervals,
|
|
258
|
+
selectedInterval,
|
|
259
|
+
selectedTimescaleID,
|
|
260
|
+
selectedIntervals,
|
|
261
|
+
timescaleLevels: levels,
|
|
262
|
+
isFullExtent: anim.isFullExtent,
|
|
263
|
+
isAnimating: anim.isAnimating,
|
|
264
|
+
reset,
|
|
265
|
+
zoomToInterval,
|
|
266
|
+
onClickTimescaleInterval,
|
|
267
|
+
timescaleIntervalStyle,
|
|
268
|
+
columnProps: {
|
|
269
|
+
showTimescale: true,
|
|
270
|
+
timescaleLevels: levels,
|
|
271
|
+
timescaleIntervalStyle,
|
|
272
|
+
onClickTimescaleInterval,
|
|
273
|
+
t_age: window?.t_age,
|
|
274
|
+
b_age: window?.b_age,
|
|
275
|
+
isTransitioning: anim.isAnimating,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** What the hook reports when zooming is switched off: no window, and no
|
|
281
|
+
* props to change how the column renders. */
|
|
282
|
+
const disabledZoom: Pick<
|
|
283
|
+
TimescaleZoom,
|
|
284
|
+
| "enabled"
|
|
285
|
+
| "window"
|
|
286
|
+
| "intervals"
|
|
287
|
+
| "selectedInterval"
|
|
288
|
+
| "isFullExtent"
|
|
289
|
+
| "isAnimating"
|
|
290
|
+
| "columnProps"
|
|
291
|
+
> = {
|
|
292
|
+
enabled: false,
|
|
293
|
+
window: null,
|
|
294
|
+
intervals: [],
|
|
295
|
+
selectedInterval: null,
|
|
296
|
+
isFullExtent: true,
|
|
297
|
+
isAnimating: false,
|
|
298
|
+
columnProps: {},
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
interface LevelWindowOptions {
|
|
302
|
+
levelWindow: number;
|
|
303
|
+
minLevel: number;
|
|
304
|
+
maxLevel: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/** A fixed window of timescale levels that slides with the selected level —
|
|
308
|
+
* one coarser for context, the rest finer to drill into. Clamped so the
|
|
309
|
+
* useless level 0 is never shown. */
|
|
310
|
+
export function levelsForSelected(
|
|
311
|
+
selectedLevel: number,
|
|
312
|
+
options: LevelWindowOptions,
|
|
313
|
+
): [number, number] {
|
|
314
|
+
const { levelWindow, minLevel, maxLevel } = options;
|
|
315
|
+
const span = Math.max(levelWindow - 1, 0);
|
|
316
|
+
const lo = Math.min(
|
|
317
|
+
Math.max(selectedLevel - 1, minLevel),
|
|
318
|
+
Math.max(maxLevel - span, minLevel),
|
|
319
|
+
);
|
|
320
|
+
return [lo, lo + span];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/** The window spanning a set of intervals. */
|
|
324
|
+
function intervalsWindow(intervals: Interval[]): AgeWindow {
|
|
325
|
+
return {
|
|
326
|
+
t_age: Math.min(...intervals.map((d) => d.lag)),
|
|
327
|
+
b_age: Math.max(...intervals.map((d) => d.eag)),
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/** The age extent of a set of units, in the shape the age-window props take.
|
|
332
|
+
* `null` for an empty or absent set. */
|
|
333
|
+
export function unitsAgeExtent(
|
|
334
|
+
units: { t_age: number; b_age: number }[] | null | undefined,
|
|
335
|
+
): AgeWindow | null {
|
|
336
|
+
if (units == null || units.length === 0) return null;
|
|
337
|
+
return {
|
|
338
|
+
t_age: Math.min(...units.map((d) => d.t_age)),
|
|
339
|
+
b_age: Math.max(...units.map((d) => d.b_age)),
|
|
340
|
+
};
|
|
341
|
+
}
|