@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.
@@ -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 = Math.abs(domain[0] - domain[1]);
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) => direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,
290
+ (s) =>
291
+ direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,
279
292
  )
280
- .sort((a, b) => direction * (nearEdge(a, direction) - nearEdge(b, direction)));
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 = direction > 0 ? section.b_age - from : from - section.t_age;
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
- /** Room for a typical unit on screen: `px` tall, whatever its duration.
32
- *
33
- * "Typical" is the median of the visible extents, which is the only summary
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.extent;
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
- return finalSectionList.map((section, i) => {
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>[] {
@@ -228,7 +228,6 @@ export function prepareColumnUnits(
228
228
  const natural = (section) =>
229
229
  scales.get(section) ?? section.scaleInfo.pixelScale;
230
230
 
231
-
232
231
  const scaleFor = natural;
233
232
 
234
233
  const window =
@@ -406,7 +406,8 @@ export function ThicknessField({ unit, label = "Thickness" }) {
406
406
  }
407
407
 
408
408
  function BibInfo({ refs }) {
409
- const refData = useMacrostratData("refs", refs);
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;