@macrostrat/column-views 3.12.1 → 3.13.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 +32 -0
- package/dist/column.cjs +15 -8
- package/dist/column.cjs.map +1 -1
- package/dist/column.d.ts +6 -0
- package/dist/column.js +15 -8
- package/dist/column.js.map +1 -1
- package/dist/prepare-units/composite-scale.cjs +16 -6
- package/dist/prepare-units/composite-scale.cjs.map +1 -1
- package/dist/prepare-units/composite-scale.d.ts +1 -1
- package/dist/prepare-units/composite-scale.js +16 -6
- package/dist/prepare-units/composite-scale.js.map +1 -1
- package/dist/prepare-units/density.cjs +21 -13
- package/dist/prepare-units/density.cjs.map +1 -1
- package/dist/prepare-units/density.d.ts +18 -11
- package/dist/prepare-units/density.js +21 -13
- package/dist/prepare-units/density.js.map +1 -1
- package/dist/prepare-units/helpers.cjs +16 -1
- package/dist/prepare-units/helpers.cjs.map +1 -1
- package/dist/prepare-units/helpers.js +16 -1
- package/dist/prepare-units/helpers.js.map +1 -1
- package/dist/prepare-units/index.cjs.map +1 -1
- package/dist/prepare-units/index.js.map +1 -1
- package/dist/unit-details/panel.cjs +1 -1
- package/dist/unit-details/panel.cjs.map +1 -1
- package/dist/unit-details/panel.js +1 -1
- package/dist/unit-details/panel.js.map +1 -1
- package/package.json +3 -3
- package/src/column.ts +28 -7
- package/src/prepare-units/composite-scale.ts +22 -9
- package/src/prepare-units/density.ts +36 -25
- package/src/prepare-units/helpers.ts +35 -1
- package/src/prepare-units/index.ts +0 -1
- package/src/unit-details/panel.ts +2 -1
|
@@ -4,7 +4,6 @@ import { ScaleContinuousNumeric, scaleLinear } from "d3-scale";
|
|
|
4
4
|
import { UnitLong } from "@macrostrat/api-types";
|
|
5
5
|
import { buildHybridScale } from "./dynamic-scales";
|
|
6
6
|
import { sectionDensity } from "./density";
|
|
7
|
-
import { ExtUnit, HybridScaleType, SectionInfo } from "./types";
|
|
8
7
|
import type {
|
|
9
8
|
ColumnScaleOptions,
|
|
10
9
|
CompositeColumnData,
|
|
@@ -14,6 +13,8 @@ import type {
|
|
|
14
13
|
PackageScaleLayoutData,
|
|
15
14
|
SectionScaleOptions,
|
|
16
15
|
} from "./types";
|
|
16
|
+
import { ExtUnit, HybridScaleType, SectionInfo } from "./types";
|
|
17
|
+
import { mergeAgeRanges, MergeMode } from "@macrostrat/stratigraphy-utils";
|
|
17
18
|
|
|
18
19
|
// Composite scale information augmented with units in each package
|
|
19
20
|
|
|
@@ -144,7 +145,13 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
144
145
|
} = opts;
|
|
145
146
|
const domain = opts.domain ?? findSectionHeightRange(data, axisType);
|
|
146
147
|
|
|
147
|
-
const dAge =
|
|
148
|
+
const dAge = dx(domain);
|
|
149
|
+
let visibleExtent = dAge; // full section is visible
|
|
150
|
+
if (visibleWindow != null) {
|
|
151
|
+
visibleExtent = dx(
|
|
152
|
+
mergeAgeRanges([domain, visibleWindow], MergeMode.Inner),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
148
155
|
|
|
149
156
|
let _pixelScale: any = opts.pixelScale;
|
|
150
157
|
let pixelHeight: number;
|
|
@@ -177,6 +184,7 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
177
184
|
* is what it's for, and never a sliver the window happens to cut. */
|
|
178
185
|
_pixelScale = sectionDensity(opts)({
|
|
179
186
|
extent: dAge,
|
|
187
|
+
visibleExtent,
|
|
180
188
|
unitExtents: visibleUnitExtents(data, axisType, visibleWindow),
|
|
181
189
|
units: data,
|
|
182
190
|
sectionID,
|
|
@@ -194,6 +202,10 @@ function buildSectionScale<T extends UnitLong>(
|
|
|
194
202
|
);
|
|
195
203
|
}
|
|
196
204
|
|
|
205
|
+
function dx(range: [number, number]) {
|
|
206
|
+
return Math.abs(range[0] - range[1]);
|
|
207
|
+
}
|
|
208
|
+
|
|
197
209
|
/**
|
|
198
210
|
* The density each section will actually render at once the window is applied.
|
|
199
211
|
*
|
|
@@ -275,9 +287,12 @@ function spendOutward<T extends UnitLong>(
|
|
|
275
287
|
// Sections ordered outward from the edge, keeping only what lies beyond it.
|
|
276
288
|
const beyond = sections
|
|
277
289
|
.filter(
|
|
278
|
-
(s) =>
|
|
290
|
+
(s) =>
|
|
291
|
+
direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,
|
|
279
292
|
)
|
|
280
|
-
.sort(
|
|
293
|
+
.sort(
|
|
294
|
+
(a, b) => direction * (nearEdge(a, direction) - nearEdge(b, direction)),
|
|
295
|
+
);
|
|
281
296
|
|
|
282
297
|
let bound = edge;
|
|
283
298
|
let budget = padding;
|
|
@@ -290,7 +305,8 @@ function spendOutward<T extends UnitLong>(
|
|
|
290
305
|
direction > 0
|
|
291
306
|
? Math.max(section.t_age, bound)
|
|
292
307
|
: Math.min(section.b_age, bound);
|
|
293
|
-
const available =
|
|
308
|
+
const available =
|
|
309
|
+
direction > 0 ? section.b_age - from : from - section.t_age;
|
|
294
310
|
if (available <= 0) continue;
|
|
295
311
|
|
|
296
312
|
const cost = available * pixelScale;
|
|
@@ -451,10 +467,7 @@ function visibleUnitExtents(
|
|
|
451
467
|
const [b_pos, t_pos] = getUnitHeightRange(d, axisType);
|
|
452
468
|
if (!clip) return Math.abs(b_pos - t_pos);
|
|
453
469
|
const [b_win, t_win] = visibleWindow;
|
|
454
|
-
return Math.max(
|
|
455
|
-
0,
|
|
456
|
-
Math.min(b_pos, b_win) - Math.max(t_pos, t_win),
|
|
457
|
-
);
|
|
470
|
+
return Math.max(0, Math.min(b_pos, b_win) - Math.max(t_pos, t_win));
|
|
458
471
|
})
|
|
459
472
|
.filter((d) => d > 0);
|
|
460
473
|
|
|
@@ -12,6 +12,8 @@ import type { UnitLong } from "@macrostrat/api-types";
|
|
|
12
12
|
export interface SectionDensityContext {
|
|
13
13
|
/** The section's extent, in axis units */
|
|
14
14
|
extent: number;
|
|
15
|
+
/** The section's shown extent, in axis units */
|
|
16
|
+
visibleExtent: number;
|
|
15
17
|
/** Extents of the units the render window actually shows, each clipped to
|
|
16
18
|
* it. Measuring what's on screen is what makes a unit-height rule mean the
|
|
17
19
|
* same thing at every zoom depth. */
|
|
@@ -28,23 +30,14 @@ export type SectionDensity = (ctx: SectionDensityContext) => number;
|
|
|
28
30
|
|
|
29
31
|
export type SectionDensityLike = number | SectionDensity;
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
* that survives both tails. Unit durations are spread over orders of
|
|
35
|
-
* magnitude: the arithmetic mean sits above any unit you would point at, so a
|
|
36
|
-
* couple of long ones squeeze everything else below the target — and the
|
|
37
|
-
* geometric mean fails the other way, since a single hair-thin unit (a
|
|
38
|
-
* Holocene sliver beside a Pliocene terrace, say) drags the log-average down
|
|
39
|
-
* and stretches the whole section to give that sliver its 20 pixels.
|
|
40
|
-
*/
|
|
41
|
-
export function unitHeight(px: number): SectionDensity {
|
|
42
|
-
return (ctx) => px / typicalExtent(ctx.unitExtents);
|
|
33
|
+
enum UnitHeightMode {
|
|
34
|
+
MEDIAN = "median",
|
|
35
|
+
MEAN = "mean",
|
|
43
36
|
}
|
|
44
37
|
|
|
45
38
|
/** Room for the section: `px` tall, whatever its extent. */
|
|
46
39
|
export function sectionHeight(px: number): SectionDensity {
|
|
47
|
-
return (ctx) => px / ctx.
|
|
40
|
+
return (ctx) => px / ctx.visibleExtent;
|
|
48
41
|
}
|
|
49
42
|
|
|
50
43
|
/** A density stated outright, in pixels per axis unit. */
|
|
@@ -75,15 +68,6 @@ export function resolveDensity(
|
|
|
75
68
|
return rule(ctx);
|
|
76
69
|
}
|
|
77
70
|
|
|
78
|
-
function typicalExtent(extents: number[]): number {
|
|
79
|
-
const sorted = extents.filter((d) => d > 0).sort((a, b) => a - b);
|
|
80
|
-
if (sorted.length === 0) return NaN;
|
|
81
|
-
const mid = (sorted.length - 1) / 2;
|
|
82
|
-
const lower = Math.floor(mid);
|
|
83
|
-
const upper = Math.ceil(mid);
|
|
84
|
-
return (sorted[lower] + sorted[upper]) / 2;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
71
|
/** Mirrors the default in `sectionDensity` */
|
|
88
72
|
export const DEFAULT_TARGET_UNIT_HEIGHT = 20;
|
|
89
73
|
/** Small sections often have a unit or two, and no room for axis labels */
|
|
@@ -131,8 +115,7 @@ export interface SectionSizeOptions {
|
|
|
131
115
|
* holds. What it returns overrides the column-wide options for that section;
|
|
132
116
|
* anything it leaves out keeps the column's value. */
|
|
133
117
|
export type SectionOptionsLike =
|
|
134
|
-
| SectionSizeOptions
|
|
135
|
-
| ((ctx: SectionDensityContext) => SectionSizeOptions);
|
|
118
|
+
SectionSizeOptions | ((ctx: SectionDensityContext) => SectionSizeOptions);
|
|
136
119
|
|
|
137
120
|
export interface SectionDensityOptions extends SectionSizeOptions {
|
|
138
121
|
/** Sizing decided per section */
|
|
@@ -164,7 +147,7 @@ function densityRule(opts: SectionSizeOptions): SectionDensity {
|
|
|
164
147
|
} = opts;
|
|
165
148
|
|
|
166
149
|
const derived = atLeast(
|
|
167
|
-
unitHeight(targetUnitHeight),
|
|
150
|
+
unitHeight(targetUnitHeight, UnitHeightMode.MEAN),
|
|
168
151
|
fixedDensity(minPixelScale),
|
|
169
152
|
sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0),
|
|
170
153
|
);
|
|
@@ -182,6 +165,34 @@ function densityRule(opts: SectionSizeOptions): SectionDensity {
|
|
|
182
165
|
};
|
|
183
166
|
}
|
|
184
167
|
|
|
168
|
+
/** Room for a typical unit on screen: `px` tall, whatever its duration.
|
|
169
|
+
*
|
|
170
|
+
* "Typical" is the median of the visible extents, which is the only summary
|
|
171
|
+
* that survives both tails. Unit durations are spread over orders of
|
|
172
|
+
* magnitude: the arithmetic mean sits above any unit you would point at, so a
|
|
173
|
+
* couple of long ones squeeze everything else below the target — and the
|
|
174
|
+
* geometric mean fails the other way, since a single hair-thin unit (a
|
|
175
|
+
* Holocene sliver beside a Pliocene terrace, say) drags the log-average down
|
|
176
|
+
* and stretches the whole section to give that sliver its 20 pixels.
|
|
177
|
+
*/
|
|
178
|
+
export function unitHeight(px: number, mode: UnitHeightMode): SectionDensity {
|
|
179
|
+
return (ctx) => px / typicalExtent(ctx.unitExtents, mode);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function typicalExtent(extents: number[], mode: UnitHeightMode): number {
|
|
183
|
+
/** The typical extent for a unit, the median of its extents. */
|
|
184
|
+
const filtered = extents.filter((d) => d > 0);
|
|
185
|
+
if (filtered.length === 0) return NaN;
|
|
186
|
+
if (mode === UnitHeightMode.MEAN) {
|
|
187
|
+
return filtered.reduce((sum, val) => sum + val, 0) / filtered.length;
|
|
188
|
+
}
|
|
189
|
+
const sorted = filtered.sort((a, b) => a - b);
|
|
190
|
+
const mid = (sorted.length - 1) / 2;
|
|
191
|
+
const lower = Math.floor(mid);
|
|
192
|
+
const upper = Math.ceil(mid);
|
|
193
|
+
return (sorted[lower] + sorted[upper]) / 2;
|
|
194
|
+
}
|
|
195
|
+
|
|
185
196
|
/** The density this axis was given, if any. The axis-specific spellings win
|
|
186
197
|
* over the generic one, which is what lets both be held while the axis
|
|
187
198
|
* changes. */
|
|
@@ -282,7 +282,7 @@ export function groupUnitsIntoSectionsByOverlap<T extends BaseUnit>(
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
const s1 = finalSectionList.map((section, i) => {
|
|
286
286
|
const [b_age, t_age] = getSectionAgeRange(section.units);
|
|
287
287
|
const [b_pos, t_pos] = getSectionPosRange(section.units, axisType);
|
|
288
288
|
return {
|
|
@@ -295,6 +295,8 @@ export function groupUnitsIntoSectionsByOverlap<T extends BaseUnit>(
|
|
|
295
295
|
units: section.units as T[],
|
|
296
296
|
};
|
|
297
297
|
});
|
|
298
|
+
|
|
299
|
+
return sortUnits(s1, axisType);
|
|
298
300
|
}
|
|
299
301
|
|
|
300
302
|
export function getSectionPosRange(
|
|
@@ -338,6 +340,38 @@ export function getSectionAgeRange(units: BaseUnit[]): [number, number] {
|
|
|
338
340
|
return getSectionPosRange(units, ColumnAxisType.AGE);
|
|
339
341
|
}
|
|
340
342
|
|
|
343
|
+
function forEachSection(fn, sections, ...args) {
|
|
344
|
+
return sections.map((section) => {
|
|
345
|
+
return {
|
|
346
|
+
...section,
|
|
347
|
+
units: fn(section.units, ...args),
|
|
348
|
+
};
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function sortUnits(units: BaseUnit[], axisType: ColumnAxisType) {
|
|
353
|
+
return units.sort((a, b) => {
|
|
354
|
+
if (
|
|
355
|
+
axisType === ColumnAxisType.HEIGHT &&
|
|
356
|
+
b.t_pos !== undefined &&
|
|
357
|
+
a.t_pos !== undefined
|
|
358
|
+
) {
|
|
359
|
+
return b.t_pos - a.t_pos;
|
|
360
|
+
}
|
|
361
|
+
if (
|
|
362
|
+
axisType === ColumnAxisType.DEPTH &&
|
|
363
|
+
b.t_pos !== undefined &&
|
|
364
|
+
a.t_pos !== undefined
|
|
365
|
+
) {
|
|
366
|
+
return a.t_pos - b.t_pos;
|
|
367
|
+
}
|
|
368
|
+
if (axisType === ColumnAxisType.AGE) {
|
|
369
|
+
return a.t_age - b.t_age;
|
|
370
|
+
}
|
|
371
|
+
return 0;
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
341
375
|
export function mergeOverlappingSections<T extends UnitLong>(
|
|
342
376
|
sections: SectionInfo<T>[],
|
|
343
377
|
): SectionInfo<T>[] {
|
|
@@ -406,7 +406,8 @@ export function ThicknessField({ unit, label = "Thickness" }) {
|
|
|
406
406
|
}
|
|
407
407
|
|
|
408
408
|
function BibInfo({ refs }) {
|
|
409
|
-
|
|
409
|
+
// A reference the server doesn't know comes back as a hole, not a record
|
|
410
|
+
const refData = useMacrostratData("refs", refs)?.filter((d) => d != null);
|
|
410
411
|
|
|
411
412
|
if (refData == null || refData.length === 0) {
|
|
412
413
|
return null;
|