@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/age-axis.cjs +1 -2
  3. package/dist/age-axis.cjs.map +1 -1
  4. package/dist/age-axis.js +1 -2
  5. package/dist/age-axis.js.map +1 -1
  6. package/dist/age-model/data.cjs +57 -0
  7. package/dist/age-model/data.cjs.map +1 -0
  8. package/dist/age-model/data.d.ts +29 -0
  9. package/dist/age-model/data.js +57 -0
  10. package/dist/age-model/data.js.map +1 -0
  11. package/dist/age-model/details.cjs +260 -0
  12. package/dist/age-model/details.cjs.map +1 -0
  13. package/dist/age-model/details.d.ts +39 -0
  14. package/dist/age-model/details.js +257 -0
  15. package/dist/age-model/details.js.map +1 -0
  16. package/dist/age-model/details.module.sass.cjs +42 -0
  17. package/dist/age-model/details.module.sass.cjs.map +1 -0
  18. package/dist/age-model/details.module.sass.js +40 -0
  19. package/dist/age-model/details.module.sass.js.map +1 -0
  20. package/dist/age-model/index.d.ts +5 -0
  21. package/dist/age-model/legacy.cjs +28 -0
  22. package/dist/age-model/legacy.cjs.map +1 -0
  23. package/dist/age-model/legacy.d.ts +6 -0
  24. package/dist/age-model/legacy.js +26 -0
  25. package/dist/age-model/legacy.js.map +1 -0
  26. package/dist/age-model/surfaces.cjs +354 -0
  27. package/dist/age-model/surfaces.cjs.map +1 -0
  28. package/dist/age-model/surfaces.d.ts +68 -0
  29. package/dist/age-model/surfaces.js +351 -0
  30. package/dist/age-model/surfaces.js.map +1 -0
  31. package/dist/age-model/surfaces.module.sass.cjs +45 -0
  32. package/dist/age-model/surfaces.module.sass.cjs.map +1 -0
  33. package/dist/age-model/surfaces.module.sass.js +43 -0
  34. package/dist/age-model/surfaces.module.sass.js.map +1 -0
  35. package/dist/age-model/types.cjs +229 -0
  36. package/dist/age-model/types.cjs.map +1 -0
  37. package/dist/age-model/types.d.ts +112 -0
  38. package/dist/age-model/types.js +229 -0
  39. package/dist/age-model/types.js.map +1 -0
  40. package/dist/column-views.css +406 -74
  41. package/dist/column.cjs +20 -13
  42. package/dist/column.cjs.map +1 -1
  43. package/dist/column.d.ts +21 -6
  44. package/dist/column.js +21 -14
  45. package/dist/column.js.map +1 -1
  46. package/dist/column.module.sass.cjs +19 -15
  47. package/dist/column.module.sass.cjs.map +1 -1
  48. package/dist/column.module.sass.js +19 -15
  49. package/dist/column.module.sass.js.map +1 -1
  50. package/dist/data-provider/store.cjs +34 -1
  51. package/dist/data-provider/store.cjs.map +1 -1
  52. package/dist/data-provider/store.d.ts +5 -0
  53. package/dist/data-provider/store.js +35 -2
  54. package/dist/data-provider/store.js.map +1 -1
  55. package/dist/index.cjs +78 -18
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.ts +3 -1
  58. package/dist/index.js +55 -4
  59. package/dist/index.js.map +1 -1
  60. package/dist/notes.cjs +11 -2
  61. package/dist/notes.cjs.map +1 -1
  62. package/dist/notes.d.ts +11 -1
  63. package/dist/notes.js +11 -2
  64. package/dist/notes.js.map +1 -1
  65. package/dist/prepare-units/composite-scale.cjs +18 -16
  66. package/dist/prepare-units/composite-scale.cjs.map +1 -1
  67. package/dist/prepare-units/composite-scale.js +18 -16
  68. package/dist/prepare-units/composite-scale.js.map +1 -1
  69. package/dist/prepare-units/density.cjs +83 -0
  70. package/dist/prepare-units/density.cjs.map +1 -0
  71. package/dist/prepare-units/density.d.ts +92 -0
  72. package/dist/prepare-units/density.js +83 -0
  73. package/dist/prepare-units/density.js.map +1 -0
  74. package/dist/prepare-units/index.cjs +15 -7
  75. package/dist/prepare-units/index.cjs.map +1 -1
  76. package/dist/prepare-units/index.d.ts +1 -0
  77. package/dist/prepare-units/index.js +15 -7
  78. package/dist/prepare-units/index.js.map +1 -1
  79. package/dist/prepare-units/types.cjs.map +1 -1
  80. package/dist/prepare-units/types.d.ts +34 -13
  81. package/dist/prepare-units/types.js.map +1 -1
  82. package/dist/section.cjs +108 -24
  83. package/dist/section.cjs.map +1 -1
  84. package/dist/section.d.ts +66 -6
  85. package/dist/section.js +109 -25
  86. package/dist/section.js.map +1 -1
  87. package/dist/timescale-zoom.cjs +191 -0
  88. package/dist/timescale-zoom.cjs.map +1 -0
  89. package/dist/timescale-zoom.d.ts +74 -0
  90. package/dist/timescale-zoom.js +191 -0
  91. package/dist/timescale-zoom.js.map +1 -0
  92. package/dist/unit-details/age-range.cjs +14 -43
  93. package/dist/unit-details/age-range.cjs.map +1 -1
  94. package/dist/unit-details/age-range.d.ts +1 -13
  95. package/dist/unit-details/age-range.js +6 -43
  96. package/dist/unit-details/age-range.js.map +1 -1
  97. package/dist/unit-details/utils.cjs +0 -5
  98. package/dist/unit-details/utils.cjs.map +1 -1
  99. package/dist/unit-details/utils.js +0 -5
  100. package/dist/unit-details/utils.js.map +1 -1
  101. package/dist/units/composite.cjs +4 -1
  102. package/dist/units/composite.cjs.map +1 -1
  103. package/dist/units/composite.d.ts +2 -0
  104. package/dist/units/composite.js +4 -1
  105. package/dist/units/composite.js.map +1 -1
  106. package/package.json +8 -8
  107. package/src/age-model/data.ts +98 -0
  108. package/src/age-model/details.module.sass +108 -0
  109. package/src/age-model/details.ts +333 -0
  110. package/src/age-model/index.ts +5 -0
  111. package/src/age-model/legacy.ts +30 -0
  112. package/src/age-model/surfaces.module.sass +168 -0
  113. package/src/age-model/surfaces.ts +568 -0
  114. package/src/age-model/types.ts +392 -0
  115. package/src/column.module.sass +59 -8
  116. package/src/column.ts +49 -23
  117. package/src/data-provider/store.ts +50 -1
  118. package/src/index.ts +5 -1
  119. package/src/notes.ts +20 -2
  120. package/src/prepare-units/composite-scale.ts +34 -31
  121. package/src/prepare-units/density.ts +197 -0
  122. package/src/prepare-units/index.ts +25 -6
  123. package/src/prepare-units/types.ts +34 -13
  124. package/src/section.ts +219 -33
  125. package/src/timescale-zoom.ts +341 -0
  126. package/src/unit-details/age-range.ts +12 -58
  127. package/src/units/composite.ts +11 -0
  128. package/dist/age-model-overlay.cjs +0 -50
  129. package/dist/age-model-overlay.cjs.map +0 -1
  130. package/dist/age-model-overlay.d.ts +0 -24
  131. package/dist/age-model-overlay.js +0 -48
  132. package/dist/age-model-overlay.js.map +0 -1
  133. package/dist/age-model-overlay.module.sass.cjs +0 -12
  134. package/dist/age-model-overlay.module.sass.cjs.map +0 -1
  135. package/dist/age-model-overlay.module.sass.js +0 -11
  136. package/dist/age-model-overlay.module.sass.js.map +0 -1
  137. package/src/age-model-overlay.module.sass +0 -11
  138. 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
+ }
@@ -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, red)
58
- fill-opacity: var(--selection-overlay-opacity, 0.5)
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, red))
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: calc(calc(1.5em + 1.5px) * var(--timescale-level-count, 4))
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
- /** Called when a timescale interval is clicked (e.g. to zoom the age range). */
69
- onClickTimescaleInterval?: TimescaleClickHandler;
70
- /** Per-interval style for the timescale (e.g. to bold the selected interval). */
71
- timescaleIntervalStyle?: IntervalStyleBuilder;
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
- /* Make pixelScale and targetUnitHeight mutually exclusive. PixelScale implies
138
- * standardization of scales in all sections */
139
- let _targetUnitHeight: number | null = targetUnitHeight;
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: _targetUnitHeight,
179
+ targetUnitHeight,
180
+ sectionOptions,
168
181
  unconformityHeight,
169
182
  pixelScale,
170
- minPixelScale: _minPixelScale,
171
- minSectionHeight: _minSectionHeight,
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,