@macrostrat/column-views 3.8.0 → 3.10.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 (173) hide show
  1. package/CHANGELOG.md +125 -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.cjs +11 -1
  8. package/dist/column.cjs.map +1 -1
  9. package/dist/column.d.ts +11 -0
  10. package/dist/column.js +11 -1
  11. package/dist/column.js.map +1 -1
  12. package/dist/correlation-chart/main.cjs +24 -12
  13. package/dist/correlation-chart/main.cjs.map +1 -1
  14. package/dist/correlation-chart/main.d.ts +8 -1
  15. package/dist/correlation-chart/main.js +24 -12
  16. package/dist/correlation-chart/main.js.map +1 -1
  17. package/dist/data-provider/store.cjs +17 -4
  18. package/dist/data-provider/store.cjs.map +1 -1
  19. package/dist/data-provider/store.d.ts +6 -1
  20. package/dist/data-provider/store.js +17 -4
  21. package/dist/data-provider/store.js.map +1 -1
  22. package/dist/index.cjs +17 -42
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.d.ts +3 -1
  25. package/dist/index.js +3 -34
  26. package/dist/index.js.map +1 -1
  27. package/dist/prepare-units/composite-scale.cjs +94 -8
  28. package/dist/prepare-units/composite-scale.cjs.map +1 -1
  29. package/dist/prepare-units/composite-scale.d.ts +39 -0
  30. package/dist/prepare-units/composite-scale.js +96 -10
  31. package/dist/prepare-units/composite-scale.js.map +1 -1
  32. package/dist/prepare-units/helpers.cjs +0 -1
  33. package/dist/prepare-units/helpers.cjs.map +1 -1
  34. package/dist/prepare-units/helpers.js +0 -1
  35. package/dist/prepare-units/helpers.js.map +1 -1
  36. package/dist/prepare-units/index.cjs +33 -12
  37. package/dist/prepare-units/index.cjs.map +1 -1
  38. package/dist/prepare-units/index.js +34 -13
  39. package/dist/prepare-units/index.js.map +1 -1
  40. package/dist/prepare-units/types.cjs.map +1 -1
  41. package/dist/prepare-units/types.d.ts +13 -0
  42. package/dist/prepare-units/types.js.map +1 -1
  43. package/dist/section.cjs +4 -2
  44. package/dist/section.cjs.map +1 -1
  45. package/dist/section.d.ts +4 -1
  46. package/dist/section.js +4 -2
  47. package/dist/section.js.map +1 -1
  48. package/dist/units/boxes.cjs +22 -39
  49. package/dist/units/boxes.cjs.map +1 -1
  50. package/dist/units/boxes.js +24 -41
  51. package/dist/units/boxes.js.map +1 -1
  52. package/dist/units/composite.cjs +12 -12
  53. package/dist/units/composite.cjs.map +1 -1
  54. package/dist/units/composite.js +12 -12
  55. package/dist/units/composite.js.map +1 -1
  56. package/dist/units/index.cjs +7 -11
  57. package/dist/units/index.cjs.map +1 -1
  58. package/dist/units/index.d.ts +0 -1
  59. package/dist/units/index.js +4 -9
  60. package/dist/units/index.js.map +1 -1
  61. package/package.json +10 -31
  62. package/src/animated-age-window.ts +120 -0
  63. package/src/column.ts +24 -0
  64. package/src/correlation-chart/main.ts +30 -8
  65. package/src/data-provider/store.ts +21 -2
  66. package/src/index.ts +4 -1
  67. package/src/prepare-units/composite-scale.ts +210 -10
  68. package/src/prepare-units/helpers.ts +0 -2
  69. package/src/prepare-units/index.ts +68 -14
  70. package/src/prepare-units/types.ts +13 -0
  71. package/src/section.ts +6 -0
  72. package/src/units/boxes.ts +37 -56
  73. package/src/units/composite.ts +21 -12
  74. package/src/units/index.ts +9 -26
  75. package/dist/maps/column-correlation/core.cjs +0 -288
  76. package/dist/maps/column-correlation/core.cjs.map +0 -1
  77. package/dist/maps/column-correlation/core.d.ts +0 -9
  78. package/dist/maps/column-correlation/core.js +0 -286
  79. package/dist/maps/column-correlation/core.js.map +0 -1
  80. package/dist/maps/column-correlation/index.d.ts +0 -2
  81. package/dist/maps/column-correlation/state.cjs +0 -193
  82. package/dist/maps/column-correlation/state.cjs.map +0 -1
  83. package/dist/maps/column-correlation/state.d.ts +0 -62
  84. package/dist/maps/column-correlation/state.js +0 -190
  85. package/dist/maps/column-correlation/state.js.map +0 -1
  86. package/dist/maps/column-navigation/index.d.ts +0 -2
  87. package/dist/maps/column-navigation/mapbox/index.cjs +0 -215
  88. package/dist/maps/column-navigation/mapbox/index.cjs.map +0 -1
  89. package/dist/maps/column-navigation/mapbox/index.d.ts +0 -38
  90. package/dist/maps/column-navigation/mapbox/index.js +0 -213
  91. package/dist/maps/column-navigation/mapbox/index.js.map +0 -1
  92. package/dist/maps/column-navigation/mapbox/keyboard-navigation.cjs +0 -105
  93. package/dist/maps/column-navigation/mapbox/keyboard-navigation.cjs.map +0 -1
  94. package/dist/maps/column-navigation/mapbox/keyboard-navigation.d.ts +0 -8
  95. package/dist/maps/column-navigation/mapbox/keyboard-navigation.js +0 -105
  96. package/dist/maps/column-navigation/mapbox/keyboard-navigation.js.map +0 -1
  97. package/dist/maps/column-navigation/mapbox/state.cjs +0 -73
  98. package/dist/maps/column-navigation/mapbox/state.cjs.map +0 -1
  99. package/dist/maps/column-navigation/mapbox/state.d.ts +0 -23
  100. package/dist/maps/column-navigation/mapbox/state.js +0 -71
  101. package/dist/maps/column-navigation/mapbox/state.js.map +0 -1
  102. package/dist/maps/column-navigation/svg/animated.d.ts +0 -1
  103. package/dist/maps/column-navigation/svg/data-fetcher.cjs +0 -25
  104. package/dist/maps/column-navigation/svg/data-fetcher.cjs.map +0 -1
  105. package/dist/maps/column-navigation/svg/data-fetcher.d.ts +0 -6
  106. package/dist/maps/column-navigation/svg/data-fetcher.js +0 -25
  107. package/dist/maps/column-navigation/svg/data-fetcher.js.map +0 -1
  108. package/dist/maps/column-navigation/svg/index.cjs +0 -94
  109. package/dist/maps/column-navigation/svg/index.cjs.map +0 -1
  110. package/dist/maps/column-navigation/svg/index.d.ts +0 -9
  111. package/dist/maps/column-navigation/svg/index.js +0 -93
  112. package/dist/maps/column-navigation/svg/index.js.map +0 -1
  113. package/dist/maps/column-navigation/svg/layers.cjs +0 -177
  114. package/dist/maps/column-navigation/svg/layers.cjs.map +0 -1
  115. package/dist/maps/column-navigation/svg/layers.d.ts +0 -54
  116. package/dist/maps/column-navigation/svg/layers.js +0 -174
  117. package/dist/maps/column-navigation/svg/layers.js.map +0 -1
  118. package/dist/maps/column-navigation/utils/keyboard-navigation.cjs +0 -50
  119. package/dist/maps/column-navigation/utils/keyboard-navigation.cjs.map +0 -1
  120. package/dist/maps/column-navigation/utils/keyboard-navigation.d.ts +0 -10
  121. package/dist/maps/column-navigation/utils/keyboard-navigation.js +0 -50
  122. package/dist/maps/column-navigation/utils/keyboard-navigation.js.map +0 -1
  123. package/dist/maps/index.d.ts +0 -4
  124. package/dist/maps/inset-map/index.cjs +0 -71
  125. package/dist/maps/inset-map/index.cjs.map +0 -1
  126. package/dist/maps/inset-map/index.d.ts +0 -16
  127. package/dist/maps/inset-map/index.js +0 -69
  128. package/dist/maps/inset-map/index.js.map +0 -1
  129. package/dist/maps/layers/columns.cjs +0 -165
  130. package/dist/maps/layers/columns.cjs.map +0 -1
  131. package/dist/maps/layers/columns.d.ts +0 -28
  132. package/dist/maps/layers/columns.js +0 -165
  133. package/dist/maps/layers/columns.js.map +0 -1
  134. package/dist/maps/layers/index.d.ts +0 -2
  135. package/dist/maps/layers/units.cjs +0 -207
  136. package/dist/maps/layers/units.cjs.map +0 -1
  137. package/dist/maps/layers/units.d.ts +0 -14
  138. package/dist/maps/layers/units.js +0 -204
  139. package/dist/maps/layers/units.js.map +0 -1
  140. package/dist/units/colors.cjs +0 -115
  141. package/dist/units/colors.cjs.map +0 -1
  142. package/dist/units/colors.d.ts +0 -20
  143. package/dist/units/colors.js +0 -113
  144. package/dist/units/colors.js.map +0 -1
  145. package/dist/units/resolvers.cjs +0 -125
  146. package/dist/units/resolvers.cjs.map +0 -1
  147. package/dist/units/resolvers.d.ts +0 -8
  148. package/dist/units/resolvers.js +0 -125
  149. package/dist/units/resolvers.js.map +0 -1
  150. package/src/maps/column-correlation/column-correlation-map.stories.ts +0 -108
  151. package/src/maps/column-correlation/core.ts +0 -342
  152. package/src/maps/column-correlation/index.ts +0 -2
  153. package/src/maps/column-correlation/state.ts +0 -315
  154. package/src/maps/column-navigation/index.ts +0 -2
  155. package/src/maps/column-navigation/mapbox/column-navigation-map.stories.ts +0 -278
  156. package/src/maps/column-navigation/mapbox/index.ts +0 -276
  157. package/src/maps/column-navigation/mapbox/keyboard-navigation.ts +0 -133
  158. package/src/maps/column-navigation/mapbox/state.ts +0 -119
  159. package/src/maps/column-navigation/svg/animated.ts +0 -78
  160. package/src/maps/column-navigation/svg/column-navigation-svg-map.stories.ts +0 -46
  161. package/src/maps/column-navigation/svg/data-fetcher.ts +0 -34
  162. package/src/maps/column-navigation/svg/index.ts +0 -96
  163. package/src/maps/column-navigation/svg/layers.ts +0 -233
  164. package/src/maps/column-navigation/utils/keyboard-navigation.ts +0 -58
  165. package/src/maps/index.ts +0 -4
  166. package/src/maps/inset-map/index.ts +0 -87
  167. package/src/maps/inset-map/inset-map.stories.ts +0 -20
  168. package/src/maps/layers/columns.ts +0 -216
  169. package/src/maps/layers/index.ts +0 -2
  170. package/src/maps/layers/units.ts +0 -286
  171. package/src/maps/unit-map.stories.ts +0 -79
  172. package/src/units/colors.ts +0 -183
  173. package/src/units/resolvers.ts +0 -141
@@ -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
@@ -9,6 +9,7 @@ import {
9
9
  TimescaleOrientation,
10
10
  TimescaleClickHandler,
11
11
  useMacrostratIntervals,
12
+ type IntervalStyleBuilder,
12
13
  } from "@macrostrat/timescale";
13
14
  import { ColumnAxisType, SVG } from "@macrostrat/column-components";
14
15
  import hyper from "@macrostrat/hyper";
@@ -203,6 +204,9 @@ function SectionUnits(props: SectionProps) {
203
204
  interface CompositeTimescaleProps {
204
205
  levels?: [number, number] | number;
205
206
  unconformityLabels?: boolean;
207
+ onClickInterval?: TimescaleClickHandler;
208
+ /** Per-interval style (e.g. to highlight the currently selected interval). */
209
+ intervalStyle?: IntervalStyleBuilder;
206
210
  }
207
211
 
208
212
  export function CompositeTimescale(props: CompositeTimescaleProps) {
@@ -228,6 +232,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
228
232
  packages,
229
233
  unconformityLabels = false,
230
234
  onClickInterval,
235
+ intervalStyle,
231
236
  } = props;
232
237
 
233
238
  // Use intervals from Macrostrat API
@@ -262,6 +267,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {
262
267
  scale,
263
268
  intervals,
264
269
  onClick: onClickInterval,
270
+ intervalStyle,
265
271
  }),
266
272
  ],
267
273
  );
@@ -2,27 +2,36 @@ import {
2
2
  ColumnAxisType,
3
3
  ColumnContext,
4
4
  ColumnLayoutContext,
5
+ ClippableRect,
5
6
  ForeignObject,
6
7
  PatternDefsProvider,
7
8
  useColumn,
8
9
  useGeologicPattern,
9
- zigZagBoxPath,
10
10
  } from "@macrostrat/column-components";
11
11
  import { SizeAwareLabel, Clickable } from "@macrostrat/ui-components";
12
12
  import hyper from "@macrostrat/hyper";
13
- import { forwardRef, ReactNode, useContext, useMemo } from "react";
14
- import { resolveID, scalePattern } from "./resolvers";
13
+ import { ReactNode, useContext, useMemo } from "react";
15
14
  import { useUnitSelectionTarget } from "../data-provider";
16
15
  import { useLithologies } from "@macrostrat/data-provider";
17
16
  import { IUnit } from "./types";
18
17
  import styles from "./boxes.module.sass";
19
18
  import classNames from "classnames";
20
19
  import { getUnitHeightRange } from "../prepare-units/utils";
21
- import { getMixedUnitColor } from "./colors";
22
20
  import type { RectBounds } from "./types";
21
+ import {
22
+ getBestFGDCPatternForUnit,
23
+ scalePattern,
24
+ getMixedUnitColor,
25
+ } from "@macrostrat/stratigraphy-utils";
23
26
 
24
27
  const h = hyper.styled(styles);
25
28
 
29
+ /** Resolve a class name through this file's CSS module exactly as the styled
30
+ * `h` would (module-scoped classes are hashed; unknown/global classes pass
31
+ * through). Needed because `ClippableRect` renders the element in
32
+ * `@macrostrat/column-components`, so class resolution must happen here. */
33
+ const cls = (name: string): string => (styles as any)[name] ?? name;
34
+
26
35
  interface UnitRectOptions {
27
36
  widthFraction?: number;
28
37
  axisType?: ColumnAxisType;
@@ -152,7 +161,7 @@ function Unit(props: UnitProps) {
152
161
  overflowTop: hasOverflowTop,
153
162
  overflowBottom: hasOverflowBottom,
154
163
  };
155
- const _patternID = patternID ?? resolveID(d);
164
+ const _patternID = patternID ?? getBestFGDCPatternForUnit(d);
156
165
  let _fill = fill ?? useGeologicPattern(_patternID, defaultFill);
157
166
 
158
167
  const hasBackgroundColor = backgroundColor != null;
@@ -170,70 +179,34 @@ function Unit(props: UnitProps) {
170
179
  },
171
180
  },
172
181
  [
173
- h(UnitRect, {
182
+ h(ClippableRect, {
174
183
  ...bounds,
175
184
  fill: backgroundColor,
176
185
  onClick,
177
- className: "background",
186
+ className: cls("background"),
178
187
  }),
179
- h(UnitRect, {
188
+ h(ClippableRect, {
180
189
  ref,
181
190
  ...bounds,
182
191
  fill: _fill,
183
192
  //mask,
184
193
  onClick,
185
- className: "unit",
194
+ className: cls("unit"),
195
+ }),
196
+ h.if(linked)(ClippableRect, {
197
+ ...bounds,
198
+ className: cls("linked-overlay"),
199
+ }),
200
+ h.if(selected)(ClippableRect, {
201
+ ...bounds,
202
+ className: cls("selection-overlay"),
186
203
  }),
187
- h.if(linked)(UnitRect, { ...bounds, className: "linked-overlay" }),
188
- h.if(selected)(UnitRect, { ...bounds, className: "selection-overlay" }),
189
204
  //defs,
190
205
  children,
191
206
  ],
192
207
  );
193
208
  }
194
209
 
195
- interface UnitRectProps {
196
- x: number;
197
- y: number;
198
- width: number;
199
- height: number;
200
- className: string;
201
- // Used to determine if the rect should have a zig-zag edge on the top or bottom
202
- overflowTop?: boolean;
203
- overflowBottom?: boolean;
204
- [key: string]: any;
205
- }
206
-
207
- const UnitRect = forwardRef((props: UnitRectProps, ref) => {
208
- const {
209
- x,
210
- y,
211
- width,
212
- height,
213
- overflowTop = false,
214
- overflowBottom = false,
215
- ...rest
216
- } = props;
217
-
218
- if (!overflowTop && !overflowBottom) {
219
- return h("rect", {
220
- x,
221
- y,
222
- width,
223
- height,
224
- ref,
225
- ...rest,
226
- });
227
- } else {
228
- const d = zigZagBoxPath(x, y, width, height, overflowTop, overflowBottom);
229
- return h("path", {
230
- d,
231
- ref,
232
- ...rest,
233
- });
234
- }
235
- });
236
-
237
210
  function LabeledUnit(props: LabeledUnitProps) {
238
211
  const {
239
212
  division,
@@ -247,12 +220,17 @@ function LabeledUnit(props: LabeledUnitProps) {
247
220
  ...baseBounds
248
221
  } = props;
249
222
 
250
- const { axisType } = useColumn();
223
+ const { axisType, isTransitioning, hideLabelsWhileTransitioning } =
224
+ useColumn();
251
225
  const bounds = {
252
226
  ...useUnitRect(division, { widthFraction, axisType }),
253
227
  ...baseBounds,
254
228
  };
255
229
  const { width, height } = bounds;
230
+ // Labels stay visible through the animation by default. `hideLabelsWhileTransitioning`
231
+ // is a perf escape hatch: when set, the `foreignObject` label (whose HTML
232
+ // reflows on every size change) is skipped mid-transition and restored on settle.
233
+ const skipLabel = isTransitioning && hideLabelsWhileTransitioning;
256
234
  return h(
257
235
  Unit,
258
236
  {
@@ -263,7 +241,7 @@ function LabeledUnit(props: LabeledUnitProps) {
263
241
  ...bounds,
264
242
  },
265
243
  [
266
- h.if(showLabel)(
244
+ h.if(showLabel && !skipLabel)(
267
245
  ForeignObject,
268
246
  { ...bounds, className: "unit-label-container" },
269
247
  h(SizeAwareLabel, {
@@ -271,6 +249,9 @@ function LabeledUnit(props: LabeledUnitProps) {
271
249
  labelClassName: "unit-label",
272
250
  style: { width, height },
273
251
  label,
252
+ // Re-fit the label when a transition settles (isTransitioning flips),
253
+ // not on every intermediate frame.
254
+ remeasureKey: isTransitioning,
274
255
  onVisibilityChanged(viz) {
275
256
  onLabelUpdated(label, viz);
276
257
  },
@@ -311,7 +292,7 @@ function UnitBoxes<T>(props: {
311
292
 
312
293
  return h(
313
294
  PatternDefsProvider,
314
- { resolveID, scalePattern },
295
+ { resolveID: getBestFGDCPatternForUnit, scalePattern },
315
296
  h("g.divisions", children),
316
297
  );
317
298
  }
@@ -42,23 +42,32 @@ export function LabelTrackerProvider(props) {
42
42
  BaseUnit[] | null
43
43
  >(null);
44
44
  const labelTrackerRef = useRef<LabelTracker>({});
45
+
46
+ const recompute = useCallback(() => {
47
+ // Recompute over the *current* unit set (which changes as the column zooms).
48
+ // The tracker ref accumulates entries for units seen at any zoom level, so
49
+ // we gate on whether every currently-visible unit has reported — not on the
50
+ // total number of tracked units, which would go stale and freeze the notes.
51
+ const tracker = labelTrackerRef.current;
52
+ const allReported = units.every((d) => findUnitID(d) in tracker);
53
+ if (!allReported) return;
54
+ setUnlabeledDivisions(units.filter((d) => tracker[findUnitID(d)] === false));
55
+ }, [units, findUnitID]);
56
+
45
57
  const trackLabelVisibility = useCallback(
46
58
  (div, visible) => {
47
- const id = findUnitID(div);
48
- labelTrackerRef.current[id] = visible;
49
- if (Object.keys(labelTrackerRef.current).length == units.length) {
50
- setUnlabeledDivisions(
51
- // @ts-ignore
52
- units.filter((d) => {
53
- const id = findUnitID(d);
54
- return labelTrackerRef.current[id] == false;
55
- }),
56
- );
57
- }
59
+ labelTrackerRef.current[findUnitID(div)] = visible;
60
+ recompute();
58
61
  },
59
- [labelTrackerRef, units, findUnitID],
62
+ [recompute, findUnitID],
60
63
  );
61
64
 
65
+ // Re-sync when the visible unit set changes (e.g. on zoom), so a unit whose
66
+ // label now fits inline stops also rendering as a note, and vice versa.
67
+ useEffect(() => {
68
+ recompute();
69
+ }, [recompute]);
70
+
62
71
  const value = trackLabelVisibility;
63
72
  return h(
64
73
  LabelTrackerContext.Provider,