@macrostrat/column-views 3.6.1 → 3.9.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.
Files changed (138) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/animated-age-window.cjs +51 -0
  3. package/dist/animated-age-window.cjs.map +1 -0
  4. package/dist/animated-age-window.d.ts +51 -0
  5. package/dist/animated-age-window.js +51 -0
  6. package/dist/animated-age-window.js.map +1 -0
  7. package/dist/column-views.css +118 -89
  8. package/dist/column.cjs +12 -3
  9. package/dist/column.cjs.map +1 -1
  10. package/dist/column.d.ts +11 -0
  11. package/dist/column.js +12 -3
  12. package/dist/column.js.map +1 -1
  13. package/dist/column.module.sass.cjs +15 -15
  14. package/dist/column.module.sass.js +15 -15
  15. package/dist/correlation-chart/main.cjs +156 -42
  16. package/dist/correlation-chart/main.cjs.map +1 -1
  17. package/dist/correlation-chart/main.d.ts +40 -2
  18. package/dist/correlation-chart/main.js +138 -23
  19. package/dist/correlation-chart/main.js.map +1 -1
  20. package/dist/correlation-chart/main.module.sass.cjs +23 -20
  21. package/dist/correlation-chart/main.module.sass.cjs.map +1 -1
  22. package/dist/correlation-chart/main.module.sass.js +23 -20
  23. package/dist/correlation-chart/main.module.sass.js.map +1 -1
  24. package/dist/correlation-chart/prepare-data.cjs +0 -1
  25. package/dist/correlation-chart/prepare-data.cjs.map +1 -1
  26. package/dist/correlation-chart/prepare-data.js +0 -1
  27. package/dist/correlation-chart/prepare-data.js.map +1 -1
  28. package/dist/data-provider/store.cjs +17 -4
  29. package/dist/data-provider/store.cjs.map +1 -1
  30. package/dist/data-provider/store.d.ts +6 -1
  31. package/dist/data-provider/store.js +17 -4
  32. package/dist/data-provider/store.js.map +1 -1
  33. package/dist/data-provider/unit-selection.cjs +4 -1
  34. package/dist/data-provider/unit-selection.cjs.map +1 -1
  35. package/dist/data-provider/unit-selection.d.ts +1 -1
  36. package/dist/data-provider/unit-selection.js +4 -1
  37. package/dist/data-provider/unit-selection.js.map +1 -1
  38. package/dist/index.cjs +16 -4
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +5 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/maps/column-correlation/core.cjs +127 -6
  44. package/dist/maps/column-correlation/core.cjs.map +1 -1
  45. package/dist/maps/column-correlation/core.js +128 -7
  46. package/dist/maps/column-correlation/core.js.map +1 -1
  47. package/dist/maps/column-correlation/state.cjs +63 -2
  48. package/dist/maps/column-correlation/state.cjs.map +1 -1
  49. package/dist/maps/column-correlation/state.d.ts +31 -1
  50. package/dist/maps/column-correlation/state.js +63 -2
  51. package/dist/maps/column-correlation/state.js.map +1 -1
  52. package/dist/maps/layers/columns.cjs +5 -1
  53. package/dist/maps/layers/columns.cjs.map +1 -1
  54. package/dist/maps/layers/columns.js +5 -1
  55. package/dist/maps/layers/columns.js.map +1 -1
  56. package/dist/prepare-units/composite-scale.cjs +94 -8
  57. package/dist/prepare-units/composite-scale.cjs.map +1 -1
  58. package/dist/prepare-units/composite-scale.d.ts +39 -0
  59. package/dist/prepare-units/composite-scale.js +96 -10
  60. package/dist/prepare-units/composite-scale.js.map +1 -1
  61. package/dist/prepare-units/helpers.cjs +0 -1
  62. package/dist/prepare-units/helpers.cjs.map +1 -1
  63. package/dist/prepare-units/helpers.js +0 -1
  64. package/dist/prepare-units/helpers.js.map +1 -1
  65. package/dist/prepare-units/index.cjs +33 -12
  66. package/dist/prepare-units/index.cjs.map +1 -1
  67. package/dist/prepare-units/index.js +34 -13
  68. package/dist/prepare-units/index.js.map +1 -1
  69. package/dist/prepare-units/types.cjs.map +1 -1
  70. package/dist/prepare-units/types.d.ts +13 -0
  71. package/dist/prepare-units/types.js.map +1 -1
  72. package/dist/section.cjs +10 -2
  73. package/dist/section.cjs.map +1 -1
  74. package/dist/section.d.ts +5 -0
  75. package/dist/section.js +10 -2
  76. package/dist/section.js.map +1 -1
  77. package/dist/unit-details/age-range.cjs +2 -1
  78. package/dist/unit-details/age-range.cjs.map +1 -1
  79. package/dist/unit-details/age-range.js +2 -1
  80. package/dist/unit-details/age-range.js.map +1 -1
  81. package/dist/unit-details/index.d.ts +1 -1
  82. package/dist/unit-details/panel.cjs +2 -3
  83. package/dist/unit-details/panel.cjs.map +1 -1
  84. package/dist/unit-details/panel.js +1 -2
  85. package/dist/unit-details/panel.js.map +1 -1
  86. package/dist/unit-details/panel.module.sass.cjs +18 -22
  87. package/dist/unit-details/panel.module.sass.cjs.map +1 -1
  88. package/dist/unit-details/panel.module.sass.js +18 -22
  89. package/dist/unit-details/panel.module.sass.js.map +1 -1
  90. package/dist/unit-details/strat-names.cjs +1 -2
  91. package/dist/unit-details/strat-names.cjs.map +1 -1
  92. package/dist/unit-details/strat-names.js +1 -2
  93. package/dist/unit-details/strat-names.js.map +1 -1
  94. package/dist/units/boxes.cjs +20 -36
  95. package/dist/units/boxes.cjs.map +1 -1
  96. package/dist/units/boxes.js +22 -38
  97. package/dist/units/boxes.js.map +1 -1
  98. package/dist/units/composite.cjs +12 -12
  99. package/dist/units/composite.cjs.map +1 -1
  100. package/dist/units/composite.js +12 -12
  101. package/dist/units/composite.js.map +1 -1
  102. package/package.json +9 -9
  103. package/src/animated-age-window.ts +120 -0
  104. package/src/column.module.sass +6 -0
  105. package/src/column.ts +24 -0
  106. package/src/correlation-chart/main.module.sass +46 -2
  107. package/src/correlation-chart/main.ts +210 -17
  108. package/src/correlation-chart/prepare-data.ts +0 -1
  109. package/src/data-provider/store.ts +21 -2
  110. package/src/data-provider/unit-selection.ts +13 -2
  111. package/src/index.ts +1 -0
  112. package/src/maps/column-correlation/column-correlation-map.stories.ts +12 -0
  113. package/src/maps/column-correlation/core.ts +155 -9
  114. package/src/maps/column-correlation/state.ts +110 -2
  115. package/src/maps/layers/columns.ts +7 -1
  116. package/src/prepare-units/composite-scale.ts +210 -10
  117. package/src/prepare-units/helpers.ts +0 -2
  118. package/src/prepare-units/index.ts +68 -14
  119. package/src/prepare-units/types.ts +13 -0
  120. package/src/section.ts +15 -1
  121. package/src/unit-details/age-range.ts +3 -1
  122. package/src/unit-details/index.ts +7 -1
  123. package/src/unit-details/panel.module.sass +0 -8
  124. package/src/unit-details/panel.ts +6 -6
  125. package/src/unit-details/strat-names.ts +1 -1
  126. package/src/units/boxes.ts +33 -53
  127. package/src/units/composite.ts +21 -12
  128. package/dist/correlation-chart/stories/utils.d.ts +0 -6
  129. package/dist/unit-details/identifiers.cjs +0 -56
  130. package/dist/unit-details/identifiers.cjs.map +0 -1
  131. package/dist/unit-details/identifiers.d.ts +0 -31
  132. package/dist/unit-details/identifiers.js +0 -54
  133. package/dist/unit-details/identifiers.js.map +0 -1
  134. package/src/correlation-chart/stories/correlation-chart.stories.ts +0 -228
  135. package/src/correlation-chart/stories/namibia-correlation.stories.ts +0 -151
  136. package/src/correlation-chart/stories/stories.module.sass +0 -18
  137. package/src/correlation-chart/stories/utils.ts +0 -24
  138. package/src/unit-details/identifiers.ts +0 -67
@@ -1,5 +1,5 @@
1
1
  import { ColumnAxisType } from "@macrostrat/column-components";
2
- import { ensureArray, getUnitHeightRange } from "./utils";
2
+ import { agesOverlap, ensureArray, getUnitHeightRange } from "./utils";
3
3
  import { ScaleContinuousNumeric, scaleLinear } from "d3-scale";
4
4
  import { UnitLong } from "@macrostrat/api-types";
5
5
  import { buildHybridScale } from "./dynamic-scales";
@@ -28,10 +28,15 @@ export function buildCompositeScaleInfo(
28
28
  let lastSectionTopHeight = 0;
29
29
 
30
30
  const packages2: PackageScaleLayoutData[] = [];
31
- for (const group of inputScales) {
31
+ for (let i = 0; i < inputScales.length; i++) {
32
+ const group = inputScales[i];
32
33
  const { domain, scale } = group;
33
34
  const [b_age, t_age] = domain;
34
- const key = `package-${b_age}-${t_age}`;
35
+ // Positionally stable key: packages are always emitted in age order, so the
36
+ // index is a stable identity. An age-based key (`package-${b_age}-${t_age}`)
37
+ // changes every frame while the age window animates, forcing every package,
38
+ // column, and timescale slice in the correlation chart to remount each frame.
39
+ const key = `package-${i}`;
35
40
 
36
41
  packages2.push({
37
42
  ...createPackageScale(group, totalHeight),
@@ -132,6 +137,7 @@ function buildSectionScale<T extends UnitLong>(
132
137
  minPixelScale = 0.2,
133
138
  axisType,
134
139
  minSectionHeight,
140
+ visibleWindow,
135
141
  scale,
136
142
  hybridScale,
137
143
  } = opts;
@@ -159,7 +165,7 @@ function buildSectionScale<T extends UnitLong>(
159
165
 
160
166
  if (scale == null) {
161
167
  if (_pixelScale == null) {
162
- const avgAgeRange = findAverageUnitHeight(data, axisType);
168
+ const avgAgeRange = findAverageUnitHeight(data, axisType, visibleWindow);
163
169
  // Get pixel height necessary to render average unit at target height
164
170
  _pixelScale = Math.max(targetUnitHeight / avgAgeRange, minPixelScale);
165
171
 
@@ -171,7 +177,11 @@ function buildSectionScale<T extends UnitLong>(
171
177
  }
172
178
 
173
179
  let height = dAge * _pixelScale;
174
- // If height is less than minSectionHeight, set it to minSectionHeight
180
+ // If height is less than minSectionHeight, set it to minSectionHeight.
181
+ // Sections reach here at their *full* extent (the rendered window is applied
182
+ // afterwards, by `trimSectionsToWindow`), so this floor only ever inflates a
183
+ // genuinely small section — which is what it's for — and never a sliver that
184
+ // the window happens to cut.
175
185
  const _minSectionHeight = minSectionHeight ?? targetUnitHeight ?? 0;
176
186
  pixelHeight = Math.max(height, _minSectionHeight);
177
187
  _pixelScale = pixelHeight / dAge;
@@ -186,6 +196,168 @@ function buildSectionScale<T extends UnitLong>(
186
196
  );
187
197
  }
188
198
 
199
+ /**
200
+ * The density each section will actually render at once the window is applied.
201
+ *
202
+ * A section the window cuts short is still being *looked at*, so
203
+ * `minSectionHeight` applies to what remains — it's honored by expanding the
204
+ * section's scale, not by showing more of it. Resolving this **before** padding
205
+ * matters: padding is a pixel margin measured at the density it will render at,
206
+ * and a section stretched to the floor afterwards would multiply the margin
207
+ * along with everything else (a 20 px margin in a band stretched 4× renders at
208
+ * 80 px, and asymmetrically so whenever one side is truncated).
209
+ *
210
+ * Sections the window leaves whole keep their laid-out density — they already
211
+ * met the floor when they were sized.
212
+ */
213
+ export function resolveWindowScales<T extends UnitLong>(
214
+ sections: SectionInfoWithScale<T>[],
215
+ focalWindow: [number, number],
216
+ floor: number,
217
+ ): Map<SectionInfoWithScale<T>, number> {
218
+ const [b_focal, t_focal] = focalWindow;
219
+ const scales = new Map<SectionInfoWithScale<T>, number>();
220
+
221
+ for (const section of sections) {
222
+ const t_age = Math.max(section.t_age, t_focal);
223
+ const b_age = Math.min(section.b_age, b_focal);
224
+ // Sections outside the focal window are visible only as padding; they keep
225
+ // their own density and render exactly the pixels asked for.
226
+ if (b_age <= t_age) continue;
227
+
228
+ const natural = section.scaleInfo.pixelScale;
229
+ if (!(natural > 0)) continue;
230
+
231
+ const isClipped = b_age < section.b_age || t_age > section.t_age;
232
+ const needed = isClipped && floor > 0 ? floor / (b_age - t_age) : 0;
233
+ scales.set(section, Math.max(natural, needed));
234
+ }
235
+ return scales;
236
+ }
237
+
238
+ /**
239
+ * Expand an age window outward by `padding` **pixels** of column, spending the
240
+ * budget against sections that are already laid out at full extent, at the
241
+ * densities they will finally render at (`scaleFor`).
242
+ *
243
+ * Because the layout is done, this is exact rather than predicted: each section
244
+ * contributes its own real pixel height. Padding collapses when the column runs
245
+ * out on that side.
246
+ *
247
+ * Unconformities between sections cost nothing. They render at a fixed height
248
+ * that's typically larger than the padding itself (30–60 px), so charging them
249
+ * would mean padding could never reach across a gap — which is the case it
250
+ * exists for: revealing the neighboring section keeps its timescale intervals
251
+ * navigable. The padding is a budget of neighboring *content*.
252
+ */
253
+ export function padWindowByPixels<T extends UnitLong>(
254
+ sections: SectionInfoWithScale<T>[],
255
+ window: [number, number],
256
+ padding: number,
257
+ scaleFor: (section: SectionInfoWithScale<T>) => number,
258
+ ): [number, number] {
259
+ if (!(padding > 0) || sections.length === 0) return window;
260
+ const [b_age, t_age] = window;
261
+ return [
262
+ spendOutward(sections, b_age, 1, padding, scaleFor),
263
+ spendOutward(sections, t_age, -1, padding, scaleFor),
264
+ ];
265
+ }
266
+
267
+ /** Walk out from one edge of the window through laid-out sections, spending a
268
+ * pixel budget, and return the age it lands on. `direction` is +1 toward older
269
+ * ages, -1 toward younger. */
270
+ function spendOutward<T extends UnitLong>(
271
+ sections: SectionInfoWithScale<T>[],
272
+ edge: number,
273
+ direction: 1 | -1,
274
+ padding: number,
275
+ scaleFor: (section: SectionInfoWithScale<T>) => number,
276
+ ): number {
277
+ // Sections ordered outward from the edge, keeping only what lies beyond it.
278
+ const beyond = sections
279
+ .filter(
280
+ (s) => direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,
281
+ )
282
+ .sort((a, b) => direction * (nearEdge(a, direction) - nearEdge(b, direction)));
283
+
284
+ let bound = edge;
285
+ let budget = padding;
286
+
287
+ for (const section of beyond) {
288
+ const pixelScale = scaleFor(section);
289
+ if (!(pixelScale > 0)) continue;
290
+
291
+ const from =
292
+ direction > 0
293
+ ? Math.max(section.t_age, bound)
294
+ : Math.min(section.b_age, bound);
295
+ const available = direction > 0 ? section.b_age - from : from - section.t_age;
296
+ if (available <= 0) continue;
297
+
298
+ const cost = available * pixelScale;
299
+ if (budget <= cost) return from + direction * (budget / pixelScale);
300
+
301
+ bound = direction > 0 ? section.b_age : section.t_age;
302
+ budget -= cost;
303
+ }
304
+ return bound;
305
+ }
306
+
307
+ /** The age at which a section starts, measured from the window edge outward. */
308
+ function nearEdge<T extends UnitLong>(
309
+ section: SectionInfoWithScale<T>,
310
+ direction: 1 | -1,
311
+ ): number {
312
+ return direction > 0 ? section.t_age : section.b_age;
313
+ }
314
+
315
+ /**
316
+ * Clip laid-out sections to the rendered window — the last step of layout, so
317
+ * that every section has already been sized (and floored) at its full extent.
318
+ * Each section renders at the density `scaleFor` resolved for it, which is what
319
+ * keeps a padding margin the same number of pixels as the band it borders is
320
+ * stretched to meet `minSectionHeight`.
321
+ */
322
+ export function trimSectionsToWindow<T extends UnitLong>(
323
+ sections: SectionInfoWithScale<T>[],
324
+ window: [number, number],
325
+ scaleFor: (section: SectionInfoWithScale<T>) => number,
326
+ ): SectionInfoWithScale<T>[] {
327
+ const [b_win, t_win] = window;
328
+ const out: SectionInfoWithScale<T>[] = [];
329
+
330
+ for (const section of sections) {
331
+ const t_age = Math.max(section.t_age, t_win);
332
+ const b_age = Math.min(section.b_age, b_win);
333
+ if (b_age <= t_age) continue;
334
+
335
+ const pixelScale = scaleFor(section);
336
+ if (
337
+ b_age === section.b_age &&
338
+ t_age === section.t_age &&
339
+ pixelScale === section.scaleInfo.pixelScale
340
+ ) {
341
+ out.push(section);
342
+ continue;
343
+ }
344
+
345
+ out.push({
346
+ ...section,
347
+ t_age,
348
+ b_age,
349
+ units: section.units.filter((u) => agesOverlap(u, { t_age, b_age })),
350
+ scaleInfo: createPackageScale({
351
+ domain: [b_age, t_age],
352
+ pixelScale,
353
+ pixelHeight: (b_age - t_age) * pixelScale,
354
+ scale: null,
355
+ }),
356
+ });
357
+ }
358
+ return out;
359
+ }
360
+
189
361
  export function createPackageScale(
190
362
  def: PackageScaleInfo,
191
363
  offset: number = 0,
@@ -260,12 +432,40 @@ function findSectionHeightRange(
260
432
  function findAverageUnitHeight(
261
433
  data: UnitLong[],
262
434
  axisType: ColumnAxisType,
435
+ visibleWindow?: [number, number] | null,
263
436
  ): number {
264
- const unitHeights = data.map((d) => {
265
- const [b_pos, t_pos] = getUnitHeightRange(d, axisType);
266
- return Math.abs(b_pos - t_pos);
267
- });
268
- return unitHeights.reduce((a, b) => a + b, 0) / unitHeights.length;
437
+ /** The typical duration of a unit, which `targetUnitHeight` sizes.
438
+ *
439
+ * Measured over what the render window actually *shows* — units outside it
440
+ * are ignored and a unit it cuts through counts only for its visible part.
441
+ * That's what makes `targetUnitHeight` mean the same thing at every zoom
442
+ * depth: the units on screen are drawn at the target height whether they're
443
+ * whole or slivers of something much longer.
444
+ *
445
+ * A section the window doesn't reach has nothing visible to measure, so it
446
+ * falls back to its own units at full duration — it's drawn at its own scale.
447
+ * (Deriving it from the padded window instead would be circular: padding is
448
+ * measured in pixels, which is what we're trying to establish.)
449
+ */
450
+ const durations = (clip: boolean) =>
451
+ data
452
+ .map((d) => {
453
+ const [b_pos, t_pos] = getUnitHeightRange(d, axisType);
454
+ if (!clip) return Math.abs(b_pos - t_pos);
455
+ const [b_win, t_win] = visibleWindow;
456
+ return Math.max(
457
+ 0,
458
+ Math.min(b_pos, b_win) - Math.max(t_pos, t_win),
459
+ );
460
+ })
461
+ .filter((d) => d > 0);
462
+
463
+ const useWindow = visibleWindow != null && axisType === ColumnAxisType.AGE;
464
+ let heights = useWindow ? durations(true) : [];
465
+ if (heights.length === 0) heights = durations(false);
466
+ if (heights.length === 0) return 1;
467
+
468
+ return heights.reduce((a, b) => a + b, 0) / heights.length;
269
469
  }
270
470
 
271
471
  export interface CompositeColumnScale {
@@ -69,8 +69,6 @@ function groupUnitsByMutualOverlap<T extends BaseUnit>(
69
69
 
70
70
  const sections = groupUnitsIntoSectionsByOverlap(units, axisType, tolerance);
71
71
 
72
- console.log("Grouped sections", sections);
73
-
74
72
  return sections.map((d) => {
75
73
  return new UnitGroup(d.units, axisType);
76
74
  });
@@ -15,6 +15,9 @@ import {
15
15
  collapseUnconformitiesByPixelHeight,
16
16
  computeSectionHeights,
17
17
  finalizeSectionHeights,
18
+ padWindowByPixels,
19
+ resolveWindowScales,
20
+ trimSectionsToWindow,
18
21
  type CompositeColumnScale,
19
22
  } from "./composite-scale";
20
23
  import {
@@ -58,6 +61,7 @@ export function prepareColumnUnits(
58
61
  collapseSmallUnconformities = false,
59
62
  hybridScale,
60
63
  scale,
64
+ windowPadding = 0,
61
65
  } = options;
62
66
 
63
67
  let _totalHeight: number | null = null;
@@ -76,19 +80,29 @@ export function prepareColumnUnits(
76
80
  }
77
81
  }
78
82
 
83
+ /** Age columns lay every section out at its full extent and clip to the
84
+ * window at the end (see below), so that a section abutting the window is
85
+ * sized by its own content rather than by the sliver that survives the clip.
86
+ * Hybrid and externally-supplied scales build their own mapping and can't be
87
+ * re-derived that way, so they keep the original clip-then-lay-out path. */
88
+ const clipBeforeLayout =
89
+ axisType != ColumnAxisType.AGE || hybridScale != null || scale != null;
90
+
79
91
  // Start by ensuring that ages and positions are numbers
80
92
  // also set up some values for eODP-style columns
81
93
  let units1 = units.map(preprocessSectionUnit);
82
94
 
83
- /** Prototype filtering to age range */
84
- units1 = units1.filter((d) => {
85
- // Filter units by t_age and b_age, inclusive
86
- if (axisType == ColumnAxisType.AGE) {
87
- return agesOverlap(d, { t_age, b_age });
88
- } else {
89
- return unitsOverlap(d, { t_pos, b_pos } as any, axisType);
90
- }
91
- });
95
+ if (clipBeforeLayout) {
96
+ /** Prototype filtering to age range */
97
+ units1 = units1.filter((d) => {
98
+ // Filter units by t_age and b_age, inclusive
99
+ if (axisType == ColumnAxisType.AGE) {
100
+ return agesOverlap(d, { t_age, b_age });
101
+ } else {
102
+ return unitsOverlap(d, { t_pos, b_pos } as any, axisType);
103
+ }
104
+ });
105
+ }
92
106
 
93
107
  let mergeMode = mergeSections;
94
108
  // if (axisType != ColumnAxisType.AGE) {
@@ -123,7 +137,7 @@ export function prepareColumnUnits(
123
137
  }
124
138
 
125
139
  // Limit sections to the range specified by t_age/b_age or t_pos/b_pos global options
126
- for (let section of sections0) {
140
+ for (let section of clipBeforeLayout ? sections0 : []) {
127
141
  if (axisType == ColumnAxisType.AGE) {
128
142
  section.t_age = Math.max(section.t_age, t_age ?? -Infinity);
129
143
  section.b_age = Math.min(section.b_age, b_age ?? Infinity);
@@ -152,11 +166,21 @@ export function prepareColumnUnits(
152
166
 
153
167
  // SCALES
154
168
 
169
+ /** The window we've been asked to render. Unit density is derived from what
170
+ * it shows (see `findAverageUnitHeight`), so it has to be known before any
171
+ * section is laid out — but nothing is *clipped* to it until the very end.
172
+ */
173
+ const focalWindow: [number, number] | null = clipBeforeLayout
174
+ ? null
175
+ : [b_age ?? Infinity, t_age ?? -Infinity];
176
+
177
+ const layoutOptions = { ...options, visibleWindow: focalWindow };
178
+
155
179
  /* Compute pixel scales etc. for sections
156
180
  * We need to do this now to determine which unconformities
157
181
  * are small enough to collapse.
158
182
  */
159
- let sectionsWithScales = computeSectionHeights(sections, options);
183
+ let sectionsWithScales = computeSectionHeights(sections, layoutOptions);
160
184
 
161
185
  if (collapseSmallUnconformities && hybridScale == null) {
162
186
  // Collapse small unconformities in pixel height space
@@ -169,7 +193,39 @@ export function prepareColumnUnits(
169
193
  sectionsWithScales = collapseUnconformitiesByPixelHeight(
170
194
  sectionsWithScales,
171
195
  threshold,
172
- options,
196
+ layoutOptions,
197
+ );
198
+ }
199
+
200
+ /** Now that every section is laid out at full extent — with its own density
201
+ * and `minSectionHeight` already settled — apply the rendered window. Padding
202
+ * is spent here, against real pixel heights, so `windowPadding` px of an
203
+ * abutting section is exactly that many pixels.
204
+ */
205
+ if (focalWindow != null && (t_age != null || b_age != null)) {
206
+ // Resolve final densities first (a section the window cuts short is
207
+ // stretched to `minSectionHeight`), then spend the padding budget against
208
+ // them, so a margin is the pixels asked for rather than those pixels times
209
+ // whatever stretch its neighbor happened to need.
210
+ const floor = options.minSectionHeight ?? options.targetUnitHeight ?? 0;
211
+ const scales = resolveWindowScales(sectionsWithScales, focalWindow, floor);
212
+ const scaleFor = (section) =>
213
+ scales.get(section) ?? section.scaleInfo.pixelScale;
214
+
215
+ const window =
216
+ windowPadding > 0
217
+ ? padWindowByPixels(
218
+ sectionsWithScales,
219
+ focalWindow,
220
+ windowPadding,
221
+ scaleFor,
222
+ )
223
+ : focalWindow;
224
+
225
+ sectionsWithScales = trimSectionsToWindow(
226
+ sectionsWithScales,
227
+ window,
228
+ scaleFor,
173
229
  );
174
230
  }
175
231
 
@@ -185,8 +241,6 @@ export function prepareColumnUnits(
185
241
  * We do this after merging sections so that we can
186
242
  * handle cases where there are overlapping units across sections
187
243
  * */
188
- console.log(sections2, "sections");
189
-
190
244
  const sectionsOut = sections2.map((section) => {
191
245
  return {
192
246
  ...section,
@@ -14,6 +14,19 @@ export interface ColumnHeightScaleOptions {
14
14
  * needed because small sections (<1-2 units) don't necessarily have space to comfortably
15
15
  * render two axis labels */
16
16
  minPixelScale?: number;
17
+ /** The requested render window, `[b_age, t_age]`, if there is one. Set
18
+ * internally by `prepareColumnUnits`: unit density is derived from the units
19
+ * this window actually shows, so `targetUnitHeight` describes the units you
20
+ * can see at any zoom depth rather than the section's overall average. */
21
+ visibleWindow?: [number, number];
22
+ /** Padding around the `t_age`/`b_age` window, in **pixels** of neighboring
23
+ * column: how much of the abutting sections to reveal past the window.
24
+ *
25
+ * Resolved after every section is laid out at full extent, so it's exact —
26
+ * each section's own density and each crossed unconformity's fixed height are
27
+ * spent from the pixel budget. Padding collapses where the column runs out.
28
+ * Ignored for hybrid or externally-supplied scales. */
29
+ windowPadding?: number;
17
30
  // Axis scale type
18
31
  axisType?: ColumnAxisType;
19
32
  // Unconformity height in pixels
package/src/section.ts CHANGED
@@ -7,7 +7,9 @@ import { ReactNode, FunctionComponent, useMemo } from "react";
7
7
  import {
8
8
  Timescale,
9
9
  TimescaleOrientation,
10
+ TimescaleClickHandler,
10
11
  useMacrostratIntervals,
12
+ type IntervalStyleBuilder,
11
13
  } from "@macrostrat/timescale";
12
14
  import { ColumnAxisType, SVG } from "@macrostrat/column-components";
13
15
  import hyper from "@macrostrat/hyper";
@@ -202,6 +204,9 @@ function SectionUnits(props: SectionProps) {
202
204
  interface CompositeTimescaleProps {
203
205
  levels?: [number, number] | number;
204
206
  unconformityLabels?: boolean;
207
+ onClickInterval?: TimescaleClickHandler;
208
+ /** Per-interval style (e.g. to highlight the currently selected interval). */
209
+ intervalStyle?: IntervalStyleBuilder;
205
210
  }
206
211
 
207
212
  export function CompositeTimescale(props: CompositeTimescaleProps) {
@@ -218,10 +223,17 @@ export function CompositeTimescale(props: CompositeTimescaleProps) {
218
223
 
219
224
  type CompositeTimescaleCoreProps = CompositeTimescaleProps & {
220
225
  packages: PackageScaleLayoutData[];
226
+ onClickInterval?: TimescaleClickHandler;
221
227
  };
222
228
 
223
229
  export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
224
- const { levels = 3, packages, unconformityLabels = false } = props;
230
+ const {
231
+ levels = 3,
232
+ packages,
233
+ unconformityLabels = false,
234
+ onClickInterval,
235
+ intervalStyle,
236
+ } = props;
225
237
 
226
238
  // Use intervals from Macrostrat API
227
239
  const baseURL = useMacrostratBaseURL();
@@ -254,6 +266,8 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
254
266
  showAgeAxis: false,
255
267
  scale,
256
268
  intervals,
269
+ onClick: onClickInterval,
270
+ intervalStyle,
257
271
  }),
258
272
  ],
259
273
  );
@@ -132,8 +132,10 @@ export function IntervalProportions({
132
132
  const i0 = unit.b_int_id;
133
133
  const i1 = unit.t_int_id;
134
134
 
135
+ const ints = new Set([i0, i1]);
136
+
135
137
  /** Get interval information */
136
- const intervalMap = useMacrostratDefs("intervals");
138
+ const intervalMap = useMacrostratDefs("intervals", Array.from(ints), null);
137
139
  const [int0, int1] = useMemo(() => {
138
140
  const int0 = intervalMap?.get(i0) ?? {};
139
141
  if (i0 === i1) {
@@ -3,4 +3,10 @@ export * from "./popover";
3
3
  export * from "./modal-panel";
4
4
  export * from "./age-range";
5
5
  export * from "./strat-names";
6
- export * from "./identifiers";
6
+
7
+ // Backwards compatibility
8
+ export {
9
+ ClickableText,
10
+ Identifier,
11
+ UnitIdentifier,
12
+ } from "@macrostrat/data-components";
@@ -61,14 +61,6 @@
61
61
  color: var(--secondary-color)
62
62
  font-style: italic
63
63
 
64
- .strat-name-id, .identifier
65
- font-family: var(--monospace-font)
66
- color: var(--secondary-color)
67
-
68
- &::before
69
- content: '#'
70
- color: var(--tertiary-color)
71
-
72
64
  .color-swatch
73
65
  width: 1em
74
66
  height: 1em
@@ -34,7 +34,11 @@ import { AgeField, Duration, IntervalProportions } from "./age-range";
34
34
  import { formatRange, formatSignificance } from "./utils.ts";
35
35
  import h from "./panel.module.sass";
36
36
  import { StratNameField } from "./strat-names.ts";
37
- import { Identifier, UnitIdentifier, UnitInfo } from "./identifiers.ts";
37
+ import {
38
+ Identifier,
39
+ UnitIdentifier,
40
+ UnitInfo,
41
+ } from "@macrostrat/data-components";
38
42
  import { LithologyIdentifier } from "@macrostrat/api-types/src/lithologies";
39
43
 
40
44
  export interface UnitDetailsPanelProps {
@@ -196,11 +200,7 @@ export enum UnitDetailsFeature {
196
200
  export type MacrostratItemClickHandler = (
197
201
  event: MouseEvent,
198
202
  item:
199
- | Lithology
200
- | Environment
201
- | UnitLong
202
- | Interval
203
- | { strat_name_id: number },
203
+ Lithology | Environment | UnitLong | Interval | { strat_name_id: number },
204
204
  ) => void;
205
205
 
206
206
  function UnitDetailsContent({
@@ -3,11 +3,11 @@ import {
3
3
  isClickable,
4
4
  ItemInteractionProps,
5
5
  useInteractionProps,
6
+ Identifier,
6
7
  } from "@macrostrat/data-components";
7
8
  import h from "./panel.module.sass";
8
9
  import classNames from "classnames";
9
10
  import { useStratNames } from "@macrostrat/data-provider";
10
- import { Identifier } from "./identifiers.ts";
11
11
 
12
12
  function useStratNameData(strat_name_id: number) {
13
13
  const stratNames = useStratNames([strat_name_id]);