@macrostrat/column-views 3.11.0 → 3.12.1
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 +184 -0
- package/dist/age-axis.cjs +1 -2
- package/dist/age-axis.cjs.map +1 -1
- package/dist/age-axis.js +1 -2
- package/dist/age-axis.js.map +1 -1
- 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 +260 -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 +257 -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 +354 -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 +351 -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 +406 -74
- 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 +78 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +55 -4
- 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 +14 -43
- package/dist/unit-details/age-range.cjs.map +1 -1
- package/dist/unit-details/age-range.d.ts +1 -13
- package/dist/unit-details/age-range.js +6 -43
- package/dist/unit-details/age-range.js.map +1 -1
- package/dist/unit-details/utils.cjs +0 -5
- package/dist/unit-details/utils.cjs.map +1 -1
- package/dist/unit-details/utils.js +0 -5
- package/dist/unit-details/utils.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 +8 -8
- 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 +12 -58
- 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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/** Surfaces: the view model for a column's age model.
|
|
2
|
+
*
|
|
3
|
+
* A `ColumnSurface` is one horizontal boundary in a column. It is what the
|
|
4
|
+
* surfaces view renders, and it is deliberately data-agnostic: a surface can
|
|
5
|
+
* come from an `/age_model` boundary (`boundaryToSurface`), be derived from
|
|
6
|
+
* unit tops and bottoms when a column has no age-model rows
|
|
7
|
+
* (`surfacesFromUnits`), or be built by an editor from its own state.
|
|
8
|
+
*/
|
|
9
|
+
import type {
|
|
10
|
+
AgeModelBoundary,
|
|
11
|
+
BoundaryStatus,
|
|
12
|
+
BoundaryType,
|
|
13
|
+
UnitLong,
|
|
14
|
+
} from "@macrostrat/api-types";
|
|
15
|
+
import { ColumnAxisType } from "@macrostrat/column-components";
|
|
16
|
+
|
|
17
|
+
export type { AgeModelBoundary, BoundaryStatus, BoundaryType };
|
|
18
|
+
|
|
19
|
+
/** A boundary status, plus `derived` for surfaces computed from units rather
|
|
20
|
+
* than stored in the age model. */
|
|
21
|
+
export type SurfaceStatus = BoundaryStatus | "derived";
|
|
22
|
+
|
|
23
|
+
/** The interval a surface is calibrated against */
|
|
24
|
+
export interface SurfaceCalibration {
|
|
25
|
+
id: number;
|
|
26
|
+
name: string;
|
|
27
|
+
b_age: number;
|
|
28
|
+
t_age: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ColumnSurface {
|
|
32
|
+
id: number | string;
|
|
33
|
+
/** Modeled age of the surface (Ma) */
|
|
34
|
+
age: number;
|
|
35
|
+
/** Measured position (height or depth) along the column, when known */
|
|
36
|
+
position?: number | null;
|
|
37
|
+
status: SurfaceStatus;
|
|
38
|
+
type: BoundaryType;
|
|
39
|
+
/** The calibration interval, and the surface's position within it
|
|
40
|
+
* (0 at the interval's base, 1 at its top) */
|
|
41
|
+
calibration?: SurfaceCalibration | null;
|
|
42
|
+
proportion?: number | null;
|
|
43
|
+
/** True when `status` was inferred rather than read from the data — see
|
|
44
|
+
* `inferTiePointStatuses`. */
|
|
45
|
+
statusInferred?: boolean;
|
|
46
|
+
unitsAbove: number[];
|
|
47
|
+
unitsBelow: number[];
|
|
48
|
+
section_id?: number | null;
|
|
49
|
+
ref_id?: number | null;
|
|
50
|
+
/** The API record, when the surface came from `/age_model` */
|
|
51
|
+
boundary?: AgeModelBoundary;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const surfaceStatuses: SurfaceStatus[] = [
|
|
55
|
+
"absolute",
|
|
56
|
+
"relative",
|
|
57
|
+
"spike",
|
|
58
|
+
"imposed",
|
|
59
|
+
"modeled",
|
|
60
|
+
"",
|
|
61
|
+
"derived",
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
/** The statuses at which the age model was actually constrained — the tie
|
|
65
|
+
* points — as opposed to surfaces whose age falls out of the model
|
|
66
|
+
* (`modeled`), carries no status, or is merely derived from units. These are
|
|
67
|
+
* the surfaces worth labeling by default. */
|
|
68
|
+
export const TIE_POINT_STATUSES: SurfaceStatus[] = [
|
|
69
|
+
"absolute",
|
|
70
|
+
"relative",
|
|
71
|
+
"spike",
|
|
72
|
+
"imposed",
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
export const surfaceStatusLabels: Record<SurfaceStatus, string> = {
|
|
76
|
+
absolute: "Absolute",
|
|
77
|
+
relative: "Relative",
|
|
78
|
+
spike: "Spike",
|
|
79
|
+
imposed: "Imposed",
|
|
80
|
+
modeled: "Modeled",
|
|
81
|
+
"": "Unspecified",
|
|
82
|
+
derived: "Derived",
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export const surfaceStatusDescriptions: Record<SurfaceStatus, string> = {
|
|
86
|
+
absolute:
|
|
87
|
+
"Independently dated; the age is authoritative and the interval position is derived from it",
|
|
88
|
+
relative:
|
|
89
|
+
"Calibrated to a position within a geologic interval; the age follows the timescale",
|
|
90
|
+
spike: "Tied to a formally defined boundary point",
|
|
91
|
+
imposed:
|
|
92
|
+
"Interval-relative representation imposed on a boundary with no chronostratigraphic tie of its own (for instance, thickness interpolation)",
|
|
93
|
+
modeled: "Interpolated by the age model between calibrated surfaces",
|
|
94
|
+
"": "No status recorded",
|
|
95
|
+
derived:
|
|
96
|
+
"Computed from unit tops and bottoms; not stored in the column's age model",
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** A CSS-safe token for a status or type (`angular unconformity` →
|
|
100
|
+
* `angular-unconformity`, `""` → `unspecified`). */
|
|
101
|
+
export function surfaceToken(value: string | null | undefined): string {
|
|
102
|
+
if (value == null || value === "") return "unspecified";
|
|
103
|
+
return value.replace(/[^a-z0-9]+/gi, "-").toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Legacy `unit_boundaries` rows use `0` as the "no unit" sentinel. */
|
|
107
|
+
export function nullifyUnitID(id: number | null | undefined): number | null {
|
|
108
|
+
if (id == null || id === 0) return null;
|
|
109
|
+
return id;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** How close to an interval's base or top a surface must sit to count as
|
|
113
|
+
* bounding it (as a fraction of the interval). */
|
|
114
|
+
export const INTERVAL_BOUND_TOLERANCE = 0.001;
|
|
115
|
+
|
|
116
|
+
/** Promote the `modeled` surfaces that cannot in fact have been interpolated
|
|
117
|
+
* to `relative`.
|
|
118
|
+
*
|
|
119
|
+
* This is a client-side affordance over a known gap in the data. An age model
|
|
120
|
+
* interpolates *between* tie points, so a surface with no constraint to
|
|
121
|
+
* interpolate from has to be one itself, whatever `boundary_status` says. Two
|
|
122
|
+
* cases are recognizable from the boundaries alone:
|
|
123
|
+
*
|
|
124
|
+
* - **It sits on the base or top of its calibration interval** (proportion 0
|
|
125
|
+
* or 1). A height referred directly to an interval bounds the model rather
|
|
126
|
+
* than falling out of it.
|
|
127
|
+
* - **It is the edge of a gap-bound package** — the youngest surface in its
|
|
128
|
+
* section with no unit above, or the oldest with none below. There is
|
|
129
|
+
* nothing beyond it to interpolate against.
|
|
130
|
+
*
|
|
131
|
+
* Both are common in columns whose age model was built by assigning positions
|
|
132
|
+
* within intervals, where the importer recorded everything as `modeled`;
|
|
133
|
+
* filtering to the tie-point statuses would otherwise hide real ones. Promoted
|
|
134
|
+
* surfaces are marked `statusInferred`, so views can say where the status came
|
|
135
|
+
* from; nothing about the underlying record changes.
|
|
136
|
+
*
|
|
137
|
+
* Two cases are deliberately left alone. A *mid-interval* position is
|
|
138
|
+
* ambiguous: an assigned position and an interpolated one are the same record
|
|
139
|
+
* there, and only the data can settle it. And an open side in the *interior*
|
|
140
|
+
* of a section is a lateral relationship, not a package edge — a unit that
|
|
141
|
+
* pinches out while others span past it — which the model can interpolate
|
|
142
|
+
* normally.
|
|
143
|
+
*/
|
|
144
|
+
export function inferTiePointStatuses(
|
|
145
|
+
surfaces: ColumnSurface[],
|
|
146
|
+
tolerance: number = INTERVAL_BOUND_TOLERANCE,
|
|
147
|
+
): ColumnSurface[] {
|
|
148
|
+
const extents = sectionAgeExtents(surfaces);
|
|
149
|
+
|
|
150
|
+
return surfaces.map((surface) => {
|
|
151
|
+
if (surface.status !== "modeled") return surface;
|
|
152
|
+
if (surface.calibration == null) return surface;
|
|
153
|
+
|
|
154
|
+
const { proportion } = surface;
|
|
155
|
+
const onIntervalBound =
|
|
156
|
+
proportion != null &&
|
|
157
|
+
(Math.abs(proportion) <= tolerance ||
|
|
158
|
+
Math.abs(proportion - 1) <= tolerance);
|
|
159
|
+
|
|
160
|
+
if (!onIntervalBound && !boundsPackage(surface, extents)) {
|
|
161
|
+
return surface;
|
|
162
|
+
}
|
|
163
|
+
return { ...surface, status: "relative", statusInferred: true };
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** An open side only means a package edge at the ends of the section. In its
|
|
168
|
+
* interior it means a unit pinching out while others span past it, which the
|
|
169
|
+
* age model interpolates like any other surface. */
|
|
170
|
+
function boundsPackage(
|
|
171
|
+
surface: ColumnSurface,
|
|
172
|
+
extents: Map<ColumnSurface["section_id"], [number, number]>,
|
|
173
|
+
): boolean {
|
|
174
|
+
const extent = extents.get(surface.section_id ?? null);
|
|
175
|
+
if (extent == null) return false;
|
|
176
|
+
const [youngest, oldest] = extent;
|
|
177
|
+
if (surface.unitsAbove.length === 0 && surface.age === youngest) return true;
|
|
178
|
+
return surface.unitsBelow.length === 0 && surface.age === oldest;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** The youngest and oldest surface age in each section. */
|
|
182
|
+
function sectionAgeExtents(
|
|
183
|
+
surfaces: ColumnSurface[],
|
|
184
|
+
): Map<ColumnSurface["section_id"], [number, number]> {
|
|
185
|
+
const extents = new Map<ColumnSurface["section_id"], [number, number]>();
|
|
186
|
+
for (const surface of surfaces) {
|
|
187
|
+
const key = surface.section_id ?? null;
|
|
188
|
+
const extent = extents.get(key);
|
|
189
|
+
if (extent == null) {
|
|
190
|
+
extents.set(key, [surface.age, surface.age]);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
extent[0] = Math.min(extent[0], surface.age);
|
|
194
|
+
extent[1] = Math.max(extent[1], surface.age);
|
|
195
|
+
}
|
|
196
|
+
return extents;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function boundaryToSurface(boundary: AgeModelBoundary): ColumnSurface {
|
|
200
|
+
const above = nullifyUnitID(boundary.unit_above);
|
|
201
|
+
const below = nullifyUnitID(boundary.unit_below);
|
|
202
|
+
let calibration: SurfaceCalibration | null = null;
|
|
203
|
+
if (boundary.interval_id != null) {
|
|
204
|
+
calibration = {
|
|
205
|
+
id: boundary.interval_id,
|
|
206
|
+
name: boundary.interval_name,
|
|
207
|
+
b_age: boundary.age_bottom,
|
|
208
|
+
t_age: boundary.age_top,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
id: boundary.boundary_id,
|
|
213
|
+
age: boundary.model_age,
|
|
214
|
+
position: boundary.boundary_position,
|
|
215
|
+
status: boundary.boundary_status ?? "",
|
|
216
|
+
type: boundary.boundary_type ?? "",
|
|
217
|
+
calibration,
|
|
218
|
+
proportion: boundary.rel_position,
|
|
219
|
+
unitsAbove: above == null ? [] : [above],
|
|
220
|
+
unitsBelow: below == null ? [] : [below],
|
|
221
|
+
section_id: boundary.section_id,
|
|
222
|
+
ref_id: boundary.ref_id,
|
|
223
|
+
boundary,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export interface SurfacesFromBoundariesOptions {
|
|
228
|
+
/** Promote `modeled` surfaces that must in fact constrain the age model to
|
|
229
|
+
* `relative` (default `true`) — see `inferTiePointStatuses`. */
|
|
230
|
+
inferTiePoints?: boolean;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Surfaces for a column's age-model boundaries, youngest first. */
|
|
234
|
+
export function surfacesFromBoundaries(
|
|
235
|
+
boundaries: AgeModelBoundary[] | null | undefined,
|
|
236
|
+
options: SurfacesFromBoundariesOptions = {},
|
|
237
|
+
): ColumnSurface[] {
|
|
238
|
+
const { inferTiePoints = true } = options;
|
|
239
|
+
if (boundaries == null) return [];
|
|
240
|
+
let surfaces = boundaries.map(boundaryToSurface);
|
|
241
|
+
if (inferTiePoints) {
|
|
242
|
+
surfaces = inferTiePointStatuses(surfaces);
|
|
243
|
+
}
|
|
244
|
+
return surfaces.sort((a, b) => a.age - b.age);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface SurfacesFromUnitsOptions {
|
|
248
|
+
/** Surfaces closer than this (in the axis's units) are merged */
|
|
249
|
+
tolerance?: number;
|
|
250
|
+
/** Which unit field the surfaces are keyed on: ages for age columns,
|
|
251
|
+
* measured positions for height and depth columns */
|
|
252
|
+
axisType?: ColumnAxisType;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Derive surfaces from unit tops and bottoms. Every distinct top or bottom
|
|
256
|
+
* becomes one surface with status `derived`; units sharing a boundary share
|
|
257
|
+
* the surface. This is the fallback when a column has no `unit_boundaries`. */
|
|
258
|
+
export function surfacesFromUnits<T extends UnitLong>(
|
|
259
|
+
units: T[] | null | undefined,
|
|
260
|
+
options: SurfacesFromUnitsOptions = {},
|
|
261
|
+
): ColumnSurface[] {
|
|
262
|
+
const { tolerance = 0.001, axisType = ColumnAxisType.AGE } = options;
|
|
263
|
+
if (units == null) return [];
|
|
264
|
+
const byPosition = isPositionAxis(axisType);
|
|
265
|
+
|
|
266
|
+
const surfaces: ColumnSurface[] = [];
|
|
267
|
+
|
|
268
|
+
function keyOf(surface: ColumnSurface): number | null {
|
|
269
|
+
if (byPosition) return surface.position ?? null;
|
|
270
|
+
return surface.age;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function addBound(unit: T, top: boolean) {
|
|
274
|
+
const age = top ? unit.t_age : unit.b_age;
|
|
275
|
+
const position = asNumber(top ? unit.t_pos : unit.b_pos);
|
|
276
|
+
const key = byPosition ? position : age;
|
|
277
|
+
if (key == null) return;
|
|
278
|
+
|
|
279
|
+
let surface = surfaces.find((d) => {
|
|
280
|
+
const k = keyOf(d);
|
|
281
|
+
return k != null && Math.abs(k - key) < tolerance;
|
|
282
|
+
});
|
|
283
|
+
if (surface == null) {
|
|
284
|
+
surface = {
|
|
285
|
+
id: "",
|
|
286
|
+
age,
|
|
287
|
+
position,
|
|
288
|
+
status: "derived",
|
|
289
|
+
type: "",
|
|
290
|
+
calibration: null,
|
|
291
|
+
proportion: null,
|
|
292
|
+
unitsAbove: [],
|
|
293
|
+
unitsBelow: [],
|
|
294
|
+
section_id: unit.section_id,
|
|
295
|
+
};
|
|
296
|
+
surfaces.push(surface);
|
|
297
|
+
}
|
|
298
|
+
// The unit whose top this is lies *below* the surface, and vice versa
|
|
299
|
+
if (top) {
|
|
300
|
+
surface.unitsBelow.push(unit.unit_id);
|
|
301
|
+
} else {
|
|
302
|
+
surface.unitsAbove.push(unit.unit_id);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const unit of units) {
|
|
307
|
+
addBound(unit, true);
|
|
308
|
+
addBound(unit, false);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
surfaces.sort((a, b) => compareAlongAxis(a, b, axisType));
|
|
312
|
+
return surfaces.map((s, i) => ({ ...s, id: `derived-${i}` }));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The coordinate a surface is drawn at for a given axis type: its age for
|
|
316
|
+
* age columns, its measured position for height and depth columns. `null`
|
|
317
|
+
* when the surface has no coordinate on that axis. */
|
|
318
|
+
export function surfacePosition(
|
|
319
|
+
surface: ColumnSurface,
|
|
320
|
+
axisType: ColumnAxisType | null | undefined,
|
|
321
|
+
): number | null {
|
|
322
|
+
if (isPositionAxis(axisType)) {
|
|
323
|
+
return surface.position ?? null;
|
|
324
|
+
}
|
|
325
|
+
return surface.age ?? null;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export function isPositionAxis(
|
|
329
|
+
axisType: ColumnAxisType | null | undefined,
|
|
330
|
+
): boolean {
|
|
331
|
+
return (
|
|
332
|
+
axisType === ColumnAxisType.HEIGHT || axisType === ColumnAxisType.DEPTH
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/** Order surfaces from the top of the column downwards. */
|
|
337
|
+
export function compareAlongAxis(
|
|
338
|
+
a: ColumnSurface,
|
|
339
|
+
b: ColumnSurface,
|
|
340
|
+
axisType: ColumnAxisType | null | undefined,
|
|
341
|
+
): number {
|
|
342
|
+
const pa = surfacePosition(a, axisType);
|
|
343
|
+
const pb = surfacePosition(b, axisType);
|
|
344
|
+
if (pa == null || pb == null) return 0;
|
|
345
|
+
if (axisType === ColumnAxisType.HEIGHT) {
|
|
346
|
+
// Heights increase upwards
|
|
347
|
+
return pb - pa;
|
|
348
|
+
}
|
|
349
|
+
// Ages and depths increase downwards
|
|
350
|
+
return pa - pb;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Percent of the way up its calibration interval, e.g. `85%`. */
|
|
354
|
+
export function formatProportion(
|
|
355
|
+
proportion: number | null | undefined,
|
|
356
|
+
): string | null {
|
|
357
|
+
if (proportion == null) return null;
|
|
358
|
+
return `${Math.round(proportion * 100)}%`;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** A short text label for a surface: its calibration ("Calymmian · 85%")
|
|
362
|
+
* or, failing that, its age. */
|
|
363
|
+
export function surfaceLabel(surface: ColumnSurface): string {
|
|
364
|
+
const { calibration } = surface;
|
|
365
|
+
if (calibration != null) {
|
|
366
|
+
const prop = formatProportion(surface.proportion);
|
|
367
|
+
if (prop != null) {
|
|
368
|
+
return `${calibration.name} · ${prop}`;
|
|
369
|
+
}
|
|
370
|
+
return calibration.name;
|
|
371
|
+
}
|
|
372
|
+
return formatAge(surface.age);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function formatAge(age: number | null | undefined): string {
|
|
376
|
+
if (age == null) return "";
|
|
377
|
+
let value = age;
|
|
378
|
+
let unit = "Ma";
|
|
379
|
+
if (Math.abs(age) < 1) {
|
|
380
|
+
value = age * 1000;
|
|
381
|
+
unit = "ka";
|
|
382
|
+
}
|
|
383
|
+
const _value = value.toLocaleString("en-US", { maximumFractionDigits: 2 });
|
|
384
|
+
return `${_value} ${unit}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function asNumber(value: unknown): number | null {
|
|
388
|
+
if (value == null || value === "") return null;
|
|
389
|
+
const n = Number(value);
|
|
390
|
+
if (Number.isNaN(n)) return null;
|
|
391
|
+
return n;
|
|
392
|
+
}
|
package/src/column.module.sass
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
// One slot per timescale level, as `--timescale-level-count` counts them
|
|
2
|
+
$timescale-column-width: calc(calc(1.5em + 1.5px) * var(--timescale-level-count, 4))
|
|
3
|
+
|
|
1
4
|
.column-container, body:global(.light-mode) .column-container
|
|
5
|
+
// One selection color for everything a column can select — units, surfaces —
|
|
6
|
+
// so the highlight means the same thing wherever it appears
|
|
7
|
+
--column-selection-color: var(--column-cursor-color, var(--pz-accent-color, #7961db))
|
|
2
8
|
--column-background-color: var(--background-color, #fff)
|
|
3
9
|
--column-stroke-color: var(--text-emphasized-color, #000)
|
|
4
10
|
--column-text-color: var(--text-emphasized-color, #000)
|
|
@@ -53,13 +59,18 @@ body:global(.dark-mode) .column-container
|
|
|
53
59
|
stroke: var(--column-stroke-color)
|
|
54
60
|
stroke-width: 1px
|
|
55
61
|
|
|
62
|
+
// A box around the selected unit: a light wash so its own color still
|
|
63
|
+
// reads, and an outline in the selection color
|
|
56
64
|
.selection-overlay
|
|
57
|
-
fill: var(--selection-overlay-color,
|
|
58
|
-
fill-opacity: var(--selection-overlay-opacity, 0.
|
|
65
|
+
fill: var(--selection-overlay-color, var(--column-selection-color))
|
|
66
|
+
fill-opacity: var(--selection-overlay-opacity, 0.15)
|
|
67
|
+
stroke: var(--selection-overlay-color, var(--column-selection-color))
|
|
68
|
+
stroke-width: var(--selection-overlay-stroke-width, 2px)
|
|
69
|
+
vector-effect: non-scaling-stroke
|
|
59
70
|
|
|
60
71
|
// Units sharing a stratigraphic name with the selection: a subtler highlight
|
|
61
72
|
.linked-overlay
|
|
62
|
-
fill: var(--linked-overlay-color, var(--selection-overlay-color,
|
|
73
|
+
fill: var(--linked-overlay-color, var(--selection-overlay-color, var(--column-selection-color)))
|
|
63
74
|
fill-opacity: var(--linked-overlay-opacity, 0.18)
|
|
64
75
|
pointer-events: none
|
|
65
76
|
|
|
@@ -80,10 +91,6 @@ body:global(.dark-mode) .column-container
|
|
|
80
91
|
.column-container :global g.height-range
|
|
81
92
|
display: none
|
|
82
93
|
|
|
83
|
-
.column-container :global .note-inner
|
|
84
|
-
border: none
|
|
85
|
-
padding: 1px
|
|
86
|
-
|
|
87
94
|
.column-container :global .frame rect
|
|
88
95
|
fill: transparent
|
|
89
96
|
|
|
@@ -99,12 +106,56 @@ body:global(.dark-mode) .column-container
|
|
|
99
106
|
cursor: pointer
|
|
100
107
|
|
|
101
108
|
.column-container :global(.timescale)
|
|
102
|
-
width:
|
|
109
|
+
width: $timescale-column-width
|
|
103
110
|
--timescale-level-size: 1.5em
|
|
104
111
|
|
|
105
112
|
.timescale-column
|
|
106
113
|
position: relative
|
|
107
114
|
|
|
115
|
+
// A section's timescales, drawn side by side against that section's scale.
|
|
116
|
+
// The name is deliberately not `timescale-container`: that is also the class
|
|
117
|
+
// on each `Timescale`'s own root, and the two were easy to confuse for one
|
|
118
|
+
// another (this one went unstyled for a while, stacking the timescales).
|
|
119
|
+
// Timescale names, set above the columns they belong to. Positioned out of
|
|
120
|
+
// flow so the timescales still start level with the units beside them: a
|
|
121
|
+
// column that shows these needs top padding to fit them.
|
|
122
|
+
.timescale-labels
|
|
123
|
+
position: absolute
|
|
124
|
+
bottom: 100%
|
|
125
|
+
left: 0
|
|
126
|
+
display: flex
|
|
127
|
+
flex-direction: row
|
|
128
|
+
align-items: flex-end
|
|
129
|
+
padding-bottom: 3px
|
|
130
|
+
|
|
131
|
+
// Matches the gap between the leveled timescale and the rest
|
|
132
|
+
> :first-child
|
|
133
|
+
margin-right: 5px
|
|
134
|
+
|
|
135
|
+
.timescale-label
|
|
136
|
+
// No font-size here: the width is in `em`, and must resolve the same way it
|
|
137
|
+
// does on the timescale below
|
|
138
|
+
width: $timescale-column-width
|
|
139
|
+
display: flex
|
|
140
|
+
justify-content: center
|
|
141
|
+
overflow: hidden
|
|
142
|
+
|
|
143
|
+
.timescale-label-text
|
|
144
|
+
writing-mode: vertical-rl
|
|
145
|
+
transform: rotate(180deg)
|
|
146
|
+
white-space: nowrap
|
|
147
|
+
font-size: 11px
|
|
148
|
+
color: var(--secondary-color, var(--text-color))
|
|
149
|
+
|
|
150
|
+
.section-timescales
|
|
151
|
+
display: flex
|
|
152
|
+
flex-direction: row
|
|
153
|
+
align-items: flex-start
|
|
154
|
+
|
|
155
|
+
// Set the timescale a column is leveled against apart from the others
|
|
156
|
+
> :first-child
|
|
157
|
+
margin-right: 5px
|
|
158
|
+
|
|
108
159
|
.section-main
|
|
109
160
|
position: relative
|
|
110
161
|
margin-bottom: -4px
|
package/src/column.ts
CHANGED
|
@@ -30,11 +30,15 @@ import {
|
|
|
30
30
|
import {
|
|
31
31
|
MacrostratColumnDataProvider,
|
|
32
32
|
useCompositeScale,
|
|
33
|
+
useLabelColumnClaimed,
|
|
33
34
|
useMacrostratColumnData,
|
|
34
35
|
} from "./data-provider";
|
|
35
36
|
import {
|
|
36
37
|
SectionSharedProps,
|
|
37
38
|
CompositeTimescale,
|
|
39
|
+
type ColumnIntervalStyleBuilder,
|
|
40
|
+
type ColumnTimescaleClickHandler,
|
|
41
|
+
type ColumnTimescaleLike,
|
|
38
42
|
SectionsColumn,
|
|
39
43
|
} from "./section";
|
|
40
44
|
import { ApproximateHeightAxis, CompositeAgeAxis } from "./age-axis";
|
|
@@ -45,10 +49,6 @@ import {
|
|
|
45
49
|
ColumnHeightScaleOptions,
|
|
46
50
|
} from "./prepare-units";
|
|
47
51
|
import { UnitLong } from "@macrostrat/api-types";
|
|
48
|
-
import type {
|
|
49
|
-
TimescaleClickHandler,
|
|
50
|
-
IntervalStyleBuilder,
|
|
51
|
-
} from "@macrostrat/timescale";
|
|
52
52
|
import { NonIdealState } from "@blueprintjs/core";
|
|
53
53
|
import { DataField } from "@macrostrat/data-components";
|
|
54
54
|
import { ScaleContinuousNumeric } from "d3-scale";
|
|
@@ -59,16 +59,32 @@ interface BaseColumnProps extends Omit<
|
|
|
59
59
|
> {
|
|
60
60
|
className?: string;
|
|
61
61
|
showLabelColumn?: boolean;
|
|
62
|
+
/** Suppress the label for a unit drawn thinner than this many pixels
|
|
63
|
+
* (default 2) */
|
|
64
|
+
labelSuppressHeight?: number;
|
|
62
65
|
keyboardNavigation?: boolean;
|
|
63
66
|
showLabels?: boolean;
|
|
64
67
|
maxInternalColumns?: number;
|
|
65
68
|
// Timescale properties
|
|
66
69
|
showTimescale?: boolean;
|
|
67
70
|
timescaleLevels?: number | [number, number];
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
/** Regional or project timescales drawn as extra columns beside the
|
|
72
|
+
* international one (see `CompositeTimescale`). Narrow `timescaleLevels` by
|
|
73
|
+
* as many to swap the finest level for them rather than widen the column. */
|
|
74
|
+
additionalTimescales?: number[];
|
|
75
|
+
/** Every timescale to draw beside the column, in order: a Macrostrat
|
|
76
|
+
* timescale ID, or a timescale with its own intervals (see
|
|
77
|
+
* `CompositeTimescale`). Supersedes `timescaleLevels` and
|
|
78
|
+
* `additionalTimescales`. */
|
|
79
|
+
timescales?: ColumnTimescaleLike[];
|
|
80
|
+
/** Draw each timescale's name above it */
|
|
81
|
+
showTimescaleLabels?: boolean;
|
|
82
|
+
/** Called when a timescale interval is clicked (e.g. to zoom the age range).
|
|
83
|
+
* The data says which timescale it was clicked in. */
|
|
84
|
+
onClickTimescaleInterval?: ColumnTimescaleClickHandler;
|
|
85
|
+
/** Per-interval style for the timescale (e.g. to bold the selected
|
|
86
|
+
* interval), by interval and the timescale it was drawn from. */
|
|
87
|
+
timescaleIntervalStyle?: ColumnIntervalStyleBuilder;
|
|
72
88
|
unconformityLabels?: boolean | UnconformityLabelPlacement;
|
|
73
89
|
onMouseOver?: (
|
|
74
90
|
unit: UnitLong | null,
|
|
@@ -119,7 +135,10 @@ export function Column(props: ColumnProps) {
|
|
|
119
135
|
b_pos,
|
|
120
136
|
unconformityHeight = 30,
|
|
121
137
|
targetUnitHeight = 20,
|
|
138
|
+
sectionOptions,
|
|
122
139
|
pixelScale,
|
|
140
|
+
pixelsPerMyr,
|
|
141
|
+
pixelsPerMeter,
|
|
123
142
|
minPixelScale = 0.2,
|
|
124
143
|
minSectionHeight = 50,
|
|
125
144
|
windowPadding = 0,
|
|
@@ -134,16 +153,9 @@ export function Column(props: ColumnProps) {
|
|
|
134
153
|
...rest
|
|
135
154
|
} = props;
|
|
136
155
|
|
|
137
|
-
/*
|
|
138
|
-
*
|
|
139
|
-
|
|
140
|
-
let _minSectionHeight = minSectionHeight;
|
|
141
|
-
let _minPixelScale = minPixelScale;
|
|
142
|
-
if (pixelScale != null) {
|
|
143
|
-
_targetUnitHeight = null;
|
|
144
|
-
_minSectionHeight = 0;
|
|
145
|
-
_minPixelScale = pixelScale;
|
|
146
|
-
}
|
|
156
|
+
/* `pixelScale` and `targetUnitHeight` are alternatives: a density stated
|
|
157
|
+
* outright takes over from the one a section's units imply, floors and all.
|
|
158
|
+
* That is settled in `prepare-units/density`, so nothing is adjusted here. */
|
|
147
159
|
|
|
148
160
|
// Handle special cases for hybrid scales (WIP, we need to regularize this)
|
|
149
161
|
let _axisType = axisType ?? ColumnAxisType.AGE;
|
|
@@ -164,11 +176,14 @@ export function Column(props: ColumnProps) {
|
|
|
164
176
|
t_pos,
|
|
165
177
|
b_pos,
|
|
166
178
|
mergeSections,
|
|
167
|
-
targetUnitHeight
|
|
179
|
+
targetUnitHeight,
|
|
180
|
+
sectionOptions,
|
|
168
181
|
unconformityHeight,
|
|
169
182
|
pixelScale,
|
|
170
|
-
|
|
171
|
-
|
|
183
|
+
pixelsPerMyr,
|
|
184
|
+
pixelsPerMeter,
|
|
185
|
+
minPixelScale,
|
|
186
|
+
minSectionHeight,
|
|
172
187
|
windowPadding,
|
|
173
188
|
collapseSmallUnconformities,
|
|
174
189
|
// TODO: consider unifying scale and hybridScale options
|
|
@@ -231,11 +246,15 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
231
246
|
width: _width = 300,
|
|
232
247
|
columnWidth: _columnWidth = 150,
|
|
233
248
|
showLabelColumn: _showLabelColumn = true,
|
|
249
|
+
labelSuppressHeight,
|
|
234
250
|
className,
|
|
235
251
|
clipUnits = false,
|
|
236
252
|
children,
|
|
237
253
|
showTimescale,
|
|
238
254
|
timescaleLevels,
|
|
255
|
+
additionalTimescales,
|
|
256
|
+
timescales,
|
|
257
|
+
showTimescaleLabels,
|
|
239
258
|
maxInternalColumns,
|
|
240
259
|
onMouseOver,
|
|
241
260
|
onClickTimescaleInterval,
|
|
@@ -244,6 +263,9 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
244
263
|
} = props;
|
|
245
264
|
|
|
246
265
|
const { axisType } = useMacrostratColumnData();
|
|
266
|
+
// A child layer drawing its own labels (the surfaces view) takes the label
|
|
267
|
+
// column over from the unit labels
|
|
268
|
+
const labelColumnClaimed = useLabelColumnClaimed();
|
|
247
269
|
|
|
248
270
|
const columnRef = useColumnRef();
|
|
249
271
|
|
|
@@ -262,12 +284,12 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
262
284
|
columnWidth = width;
|
|
263
285
|
}
|
|
264
286
|
let showLabelColumn = _showLabelColumn;
|
|
265
|
-
if (columnWidth > width - 10) {
|
|
287
|
+
if (columnWidth > width - 10 || labelColumnClaimed) {
|
|
266
288
|
showLabelColumn = false;
|
|
267
289
|
}
|
|
268
290
|
|
|
269
291
|
let _showTimescale = showTimescale ?? true;
|
|
270
|
-
if (timescaleLevels != null) {
|
|
292
|
+
if (timescaleLevels != null || timescales != null) {
|
|
271
293
|
_showTimescale = true;
|
|
272
294
|
}
|
|
273
295
|
_showTimescale = axisType == ColumnAxisType.AGE && _showTimescale;
|
|
@@ -283,6 +305,9 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
283
305
|
h(ageAxisComponent),
|
|
284
306
|
h.if(_showTimescale)(CompositeTimescale, {
|
|
285
307
|
levels: timescaleLevels,
|
|
308
|
+
additionalTimescales,
|
|
309
|
+
timescales,
|
|
310
|
+
showLabels: showTimescaleLabels,
|
|
286
311
|
unconformityLabels: _timescaleUnconformityLabels,
|
|
287
312
|
onClickInterval: onClickTimescaleInterval,
|
|
288
313
|
intervalStyle: timescaleIntervalStyle,
|
|
@@ -293,6 +318,7 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
293
318
|
width,
|
|
294
319
|
columnWidth,
|
|
295
320
|
showLabelColumn,
|
|
321
|
+
labelSuppressHeight,
|
|
296
322
|
clipUnits,
|
|
297
323
|
unconformityLabels: _sectionUnconformityLabels,
|
|
298
324
|
maxInternalColumns,
|