@macrostrat/column-views 3.11.0 → 3.12.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 (130) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/dist/age-model/data.cjs +57 -0
  3. package/dist/age-model/data.cjs.map +1 -0
  4. package/dist/age-model/data.d.ts +29 -0
  5. package/dist/age-model/data.js +57 -0
  6. package/dist/age-model/data.js.map +1 -0
  7. package/dist/age-model/details.cjs +261 -0
  8. package/dist/age-model/details.cjs.map +1 -0
  9. package/dist/age-model/details.d.ts +39 -0
  10. package/dist/age-model/details.js +258 -0
  11. package/dist/age-model/details.js.map +1 -0
  12. package/dist/age-model/details.module.sass.cjs +42 -0
  13. package/dist/age-model/details.module.sass.cjs.map +1 -0
  14. package/dist/age-model/details.module.sass.js +40 -0
  15. package/dist/age-model/details.module.sass.js.map +1 -0
  16. package/dist/age-model/index.d.ts +5 -0
  17. package/dist/age-model/legacy.cjs +28 -0
  18. package/dist/age-model/legacy.cjs.map +1 -0
  19. package/dist/age-model/legacy.d.ts +6 -0
  20. package/dist/age-model/legacy.js +26 -0
  21. package/dist/age-model/legacy.js.map +1 -0
  22. package/dist/age-model/surfaces.cjs +355 -0
  23. package/dist/age-model/surfaces.cjs.map +1 -0
  24. package/dist/age-model/surfaces.d.ts +68 -0
  25. package/dist/age-model/surfaces.js +352 -0
  26. package/dist/age-model/surfaces.js.map +1 -0
  27. package/dist/age-model/surfaces.module.sass.cjs +45 -0
  28. package/dist/age-model/surfaces.module.sass.cjs.map +1 -0
  29. package/dist/age-model/surfaces.module.sass.js +43 -0
  30. package/dist/age-model/surfaces.module.sass.js.map +1 -0
  31. package/dist/age-model/types.cjs +229 -0
  32. package/dist/age-model/types.cjs.map +1 -0
  33. package/dist/age-model/types.d.ts +112 -0
  34. package/dist/age-model/types.js +229 -0
  35. package/dist/age-model/types.js.map +1 -0
  36. package/dist/column-views.css +389 -57
  37. package/dist/column.cjs +20 -13
  38. package/dist/column.cjs.map +1 -1
  39. package/dist/column.d.ts +21 -6
  40. package/dist/column.js +21 -14
  41. package/dist/column.js.map +1 -1
  42. package/dist/column.module.sass.cjs +19 -15
  43. package/dist/column.module.sass.cjs.map +1 -1
  44. package/dist/column.module.sass.js +19 -15
  45. package/dist/column.module.sass.js.map +1 -1
  46. package/dist/data-provider/store.cjs +34 -1
  47. package/dist/data-provider/store.cjs.map +1 -1
  48. package/dist/data-provider/store.d.ts +5 -0
  49. package/dist/data-provider/store.js +35 -2
  50. package/dist/data-provider/store.js.map +1 -1
  51. package/dist/index.cjs +54 -3
  52. package/dist/index.cjs.map +1 -1
  53. package/dist/index.d.ts +3 -1
  54. package/dist/index.js +54 -3
  55. package/dist/index.js.map +1 -1
  56. package/dist/notes.cjs +11 -2
  57. package/dist/notes.cjs.map +1 -1
  58. package/dist/notes.d.ts +11 -1
  59. package/dist/notes.js +11 -2
  60. package/dist/notes.js.map +1 -1
  61. package/dist/prepare-units/composite-scale.cjs +18 -16
  62. package/dist/prepare-units/composite-scale.cjs.map +1 -1
  63. package/dist/prepare-units/composite-scale.js +18 -16
  64. package/dist/prepare-units/composite-scale.js.map +1 -1
  65. package/dist/prepare-units/density.cjs +83 -0
  66. package/dist/prepare-units/density.cjs.map +1 -0
  67. package/dist/prepare-units/density.d.ts +92 -0
  68. package/dist/prepare-units/density.js +83 -0
  69. package/dist/prepare-units/density.js.map +1 -0
  70. package/dist/prepare-units/index.cjs +15 -7
  71. package/dist/prepare-units/index.cjs.map +1 -1
  72. package/dist/prepare-units/index.d.ts +1 -0
  73. package/dist/prepare-units/index.js +15 -7
  74. package/dist/prepare-units/index.js.map +1 -1
  75. package/dist/prepare-units/types.cjs.map +1 -1
  76. package/dist/prepare-units/types.d.ts +34 -13
  77. package/dist/prepare-units/types.js.map +1 -1
  78. package/dist/section.cjs +108 -24
  79. package/dist/section.cjs.map +1 -1
  80. package/dist/section.d.ts +66 -6
  81. package/dist/section.js +109 -25
  82. package/dist/section.js.map +1 -1
  83. package/dist/timescale-zoom.cjs +191 -0
  84. package/dist/timescale-zoom.cjs.map +1 -0
  85. package/dist/timescale-zoom.d.ts +74 -0
  86. package/dist/timescale-zoom.js +191 -0
  87. package/dist/timescale-zoom.js.map +1 -0
  88. package/dist/unit-details/age-range.cjs +1 -0
  89. package/dist/unit-details/age-range.cjs.map +1 -1
  90. package/dist/unit-details/age-range.d.ts +5 -1
  91. package/dist/unit-details/age-range.js +1 -0
  92. package/dist/unit-details/age-range.js.map +1 -1
  93. package/dist/units/composite.cjs +4 -1
  94. package/dist/units/composite.cjs.map +1 -1
  95. package/dist/units/composite.d.ts +2 -0
  96. package/dist/units/composite.js +4 -1
  97. package/dist/units/composite.js.map +1 -1
  98. package/package.json +7 -7
  99. package/src/age-model/data.ts +98 -0
  100. package/src/age-model/details.module.sass +108 -0
  101. package/src/age-model/details.ts +333 -0
  102. package/src/age-model/index.ts +5 -0
  103. package/src/age-model/legacy.ts +30 -0
  104. package/src/age-model/surfaces.module.sass +168 -0
  105. package/src/age-model/surfaces.ts +568 -0
  106. package/src/age-model/types.ts +392 -0
  107. package/src/column.module.sass +59 -8
  108. package/src/column.ts +49 -23
  109. package/src/data-provider/store.ts +50 -1
  110. package/src/index.ts +5 -1
  111. package/src/notes.ts +20 -2
  112. package/src/prepare-units/composite-scale.ts +34 -31
  113. package/src/prepare-units/density.ts +197 -0
  114. package/src/prepare-units/index.ts +25 -6
  115. package/src/prepare-units/types.ts +34 -13
  116. package/src/section.ts +219 -33
  117. package/src/timescale-zoom.ts +341 -0
  118. package/src/unit-details/age-range.ts +3 -3
  119. package/src/units/composite.ts +11 -0
  120. package/dist/age-model-overlay.cjs +0 -50
  121. package/dist/age-model-overlay.cjs.map +0 -1
  122. package/dist/age-model-overlay.d.ts +0 -24
  123. package/dist/age-model-overlay.js +0 -48
  124. package/dist/age-model-overlay.js.map +0 -1
  125. package/dist/age-model-overlay.module.sass.cjs +0 -12
  126. package/dist/age-model-overlay.module.sass.cjs.map +0 -1
  127. package/dist/age-model-overlay.module.sass.js +0 -11
  128. package/dist/age-model-overlay.module.sass.js.map +0 -1
  129. package/src/age-model-overlay.module.sass +0 -11
  130. package/src/age-model-overlay.ts +0 -96
@@ -2,8 +2,10 @@ import {
2
2
  createContext,
3
3
  ReactNode,
4
4
  RefObject,
5
+ useCallback,
5
6
  useContext,
6
7
  useMemo,
8
+ useState,
7
9
  } from "react";
8
10
  import h from "@macrostrat/hyper";
9
11
  import {
@@ -188,11 +190,58 @@ export function MacrostratColumnDataProvider<T extends BaseUnit>({
188
190
  },
189
191
  [
190
192
  h(ColumnRefManager, { ref }),
191
- h(MacrostratColumnDataContext.Provider, { value }, children),
193
+ h(
194
+ MacrostratColumnDataContext.Provider,
195
+ { value },
196
+ h(ColumnLayoutOverridesProvider, null, children),
197
+ ),
192
198
  ],
193
199
  );
194
200
  }
195
201
 
202
+ /* Layout overrides requested by a column's children. A layer that draws its
203
+ * own labels beside the units (the surfaces view) claims the label column, so
204
+ * the unit labels give way: it is one set of labels or the other. */
205
+
206
+ interface ColumnLayoutOverrides {
207
+ /** How many children currently claim the label column */
208
+ labelColumnClaims: number;
209
+ claimLabelColumn(): () => void;
210
+ }
211
+
212
+ const ColumnLayoutOverridesContext = createContext<ColumnLayoutOverrides>({
213
+ labelColumnClaims: 0,
214
+ claimLabelColumn: () => () => {},
215
+ });
216
+
217
+ function ColumnLayoutOverridesProvider({ children }: { children: ReactNode }) {
218
+ const [labelColumnClaims, setClaims] = useState(0);
219
+ const claimLabelColumn = useCallback(() => {
220
+ setClaims((n) => n + 1);
221
+ return () => setClaims((n) => Math.max(n - 1, 0));
222
+ }, []);
223
+ const value = useMemo(
224
+ () => ({ labelColumnClaims, claimLabelColumn }),
225
+ [labelColumnClaims, claimLabelColumn],
226
+ );
227
+ return h(ColumnLayoutOverridesContext.Provider, { value }, children);
228
+ }
229
+
230
+ /** Claim the column's label column for this component while it is mounted:
231
+ * the unit labels are hidden in favor of whatever the caller draws there. */
232
+ export function useClaimLabelColumn(active: boolean = true) {
233
+ const { claimLabelColumn } = useContext(ColumnLayoutOverridesContext);
234
+ useEffect(() => {
235
+ if (!active) return;
236
+ return claimLabelColumn();
237
+ }, [active, claimLabelColumn]);
238
+ }
239
+
240
+ /** Whether a child of the column has claimed the label column */
241
+ export function useLabelColumnClaimed(): boolean {
242
+ return useContext(ColumnLayoutOverridesContext).labelColumnClaims > 0;
243
+ }
244
+
196
245
  function ColumnRefManager({ ref }: { ref?: RefObject<ColumnRef> }) {
197
246
  const selectedUnitElement = scope.useAtomValue(selectedUnitElementAtom);
198
247
  useEffect(() => {
package/src/index.ts CHANGED
@@ -3,9 +3,13 @@ export * from "./data-provider";
3
3
  export * from "./age-axis";
4
4
  export * from "./prepare-units";
5
5
  export * from "./animated-age-window";
6
+ export * from "./timescale-zoom";
6
7
  export * from "./column";
8
+ // The timescale column on its own: a view can add one per timescale its data
9
+ // refers to, drawn against the same section scales
10
+ export { CompositeTimescale, CompositeTimescaleCore } from "./section";
7
11
  export * from "./unit-details";
8
- export * from "./age-model-overlay";
12
+ export * from "./age-model";
9
13
  export * from "./correlation-chart";
10
14
  export * from "./notes";
11
15
  export * from "./facets";
package/src/notes.ts CHANGED
@@ -2,7 +2,7 @@ import h from "@macrostrat/hyper";
2
2
 
3
3
  import { ColumnNotesProvider } from "./units";
4
4
 
5
- import { NotesColumn, SVG } from "@macrostrat/column-components";
5
+ import { NotesColumn, SVG, type NoteData } from "@macrostrat/column-components";
6
6
  import { useCompositeScale, useMacrostratColumnData } from "./data-provider";
7
7
  import type { ComponentType, ReactNode } from "react";
8
8
 
@@ -14,6 +14,15 @@ interface ColumnNotesProps {
14
14
  deltaConnectorAttachment?: number;
15
15
  children?: ReactNode;
16
16
  focusedNoteComponent?: ComponentType<any> | null;
17
+ /** Called when a note is clicked */
18
+ onClickNote?: (note: NoteData) => void;
19
+ /** Px the connector continues past each end (see `NodeConnectorOptions`) */
20
+ connectorOverhang?: number | [number, number];
21
+ /** Draw the marker at a point note's height (default true) */
22
+ showPointMarker?: boolean;
23
+ /** Options for the label force layout (e.g. `nodeSpacing`) */
24
+ forceOptions?: object;
25
+ className?: string;
17
26
  }
18
27
 
19
28
  export function ColumnNotes({
@@ -23,6 +32,11 @@ export function ColumnNotes({
23
32
  paddingLeft = 60,
24
33
  deltaConnectorAttachment,
25
34
  focusedNoteComponent,
35
+ onClickNote,
36
+ forceOptions,
37
+ connectorOverhang,
38
+ showPointMarker,
39
+ className,
26
40
  children,
27
41
  }: ColumnNotesProps) {
28
42
  const { totalHeight } = useMacrostratColumnData();
@@ -36,7 +50,7 @@ export function ColumnNotes({
36
50
  pixelScale: -1,
37
51
  },
38
52
  [
39
- h(SVG, { width, height: totalHeight, paddingH: 4 }, [
53
+ h(SVG, { width, height: totalHeight, paddingH: 4, className }, [
40
54
  h(NotesColumn, {
41
55
  width,
42
56
  notes,
@@ -44,6 +58,10 @@ export function ColumnNotes({
44
58
  paddingLeft,
45
59
  deltaConnectorAttachment,
46
60
  focusedNoteComponent,
61
+ onClickNote,
62
+ forceOptions,
63
+ connectorOverhang,
64
+ showPointMarker,
47
65
  }),
48
66
  ]),
49
67
  children,
@@ -3,6 +3,7 @@ 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";
6
+ import { sectionDensity } from "./density";
6
7
  import { ExtUnit, HybridScaleType, SectionInfo } from "./types";
7
8
  import type {
8
9
  ColumnScaleOptions,
@@ -120,6 +121,7 @@ function addScaleToSection<T extends UnitLong = ExtUnit>(
120
121
  const scaleInfo = buildSectionScale<T>(units, {
121
122
  ...opts,
122
123
  domain: _range,
124
+ sectionID: group.section_id,
123
125
  });
124
126
 
125
127
  return {
@@ -134,18 +136,17 @@ function buildSectionScale<T extends UnitLong>(
134
136
  ): PackageScaleInfo {
135
137
  const {
136
138
  targetUnitHeight = 20,
137
- minPixelScale = 0.2,
138
139
  axisType,
139
- minSectionHeight,
140
140
  visibleWindow,
141
141
  scale,
142
142
  hybridScale,
143
+ sectionID,
143
144
  } = opts;
144
145
  const domain = opts.domain ?? findSectionHeightRange(data, axisType);
145
146
 
146
147
  const dAge = Math.abs(domain[0] - domain[1]);
147
148
 
148
- let _pixelScale = opts.pixelScale;
149
+ let _pixelScale: any = opts.pixelScale;
149
150
  let pixelHeight: number;
150
151
 
151
152
  if (hybridScale != null) {
@@ -154,7 +155,12 @@ function buildSectionScale<T extends UnitLong>(
154
155
  * This is somewhat like an ordinal scale
155
156
  */
156
157
  if (hybridScale.type === HybridScaleType.EquidistantSurfaces) {
158
+ // Pixels between one surface and the next: neither axis's units
157
159
  _pixelScale ??= targetUnitHeight;
160
+ } else {
161
+ // An approximate-height column is drawn from measured thickness, so it
162
+ // is metres per pixel whatever the axis is labeled in
163
+ _pixelScale = opts.pixelsPerMeter ?? _pixelScale;
158
164
  }
159
165
 
160
166
  return buildHybridScale(hybridScale, data, domain, {
@@ -164,27 +170,19 @@ function buildSectionScale<T extends UnitLong>(
164
170
  }
165
171
 
166
172
  if (scale == null) {
167
- if (_pixelScale == null) {
168
- const avgAgeRange = findAverageUnitHeight(data, axisType, visibleWindow);
169
- // Get pixel height necessary to render average unit at target height
170
- _pixelScale = Math.max(targetUnitHeight / avgAgeRange, minPixelScale);
171
-
172
- // OLD METHOD that cares about overall section height vs. individual unit height
173
- // 0.2 pixel per myr is the floor scale
174
- //const targetHeight = targetUnitHeight * data.length;
175
- // 1 pixel per myr is the floor scale
176
- //_pixelScale = Math.max(targetHeight / dAge, minPixelScale);
177
- }
178
-
179
- let height = dAge * _pixelScale;
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.
185
- const _minSectionHeight = minSectionHeight ?? targetUnitHeight ?? 0;
186
- pixelHeight = Math.max(height, _minSectionHeight);
187
- _pixelScale = pixelHeight / dAge;
173
+ /** Every rule that sets a section's height resolves to one density (see
174
+ * `./density`). Sections reach here at their *full* extent — the rendered
175
+ * window is applied afterwards, by `trimSectionsToWindow` — so a floor on
176
+ * the section's height only ever inflates a genuinely small section, which
177
+ * is what it's for, and never a sliver the window happens to cut. */
178
+ _pixelScale = sectionDensity(opts)({
179
+ extent: dAge,
180
+ unitExtents: visibleUnitExtents(data, axisType, visibleWindow),
181
+ units: data,
182
+ sectionID,
183
+ axisType,
184
+ });
185
+ pixelHeight = dAge * _pixelScale;
188
186
  } else {
189
187
  // If a scale is provided, use it to compute pixel height
190
188
  pixelHeight = Math.abs(scale(domain[0]) - scale(domain[1]));
@@ -429,12 +427,12 @@ function findSectionHeightRange(
429
427
  }
430
428
  }
431
429
 
432
- function findAverageUnitHeight(
430
+ function visibleUnitExtents(
433
431
  data: UnitLong[],
434
432
  axisType: ColumnAxisType,
435
433
  visibleWindow?: [number, number] | null,
436
- ): number {
437
- /** The typical duration of a unit, which `targetUnitHeight` sizes.
434
+ ): number[] {
435
+ /** The durations of the units a density rule gets to size.
438
436
  *
439
437
  * Measured over what the render window actually *shows* — units outside it
440
438
  * are ignored and a unit it cuts through counts only for its visible part.
@@ -463,9 +461,7 @@ function findAverageUnitHeight(
463
461
  const useWindow = visibleWindow != null && axisType === ColumnAxisType.AGE;
464
462
  let heights = useWindow ? durations(true) : [];
465
463
  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;
464
+ return heights;
469
465
  }
470
466
 
471
467
  export interface CompositeColumnScale {
@@ -593,7 +589,14 @@ export function collapseUnconformitiesByPixelHeight<T extends UnitLong>(
593
589
  _diff(heights.map(currentSection.scaleInfo.scale)),
594
590
  ];
595
591
 
596
- const pxHeight = Math.min(...pxHeights);
592
+ /** The gap has no density of its own — it falls between two sections that
593
+ * may be drawn at very different ones — so it is judged at the finer of
594
+ * the two. Taking the smaller estimate let a sparse neighbor speak for a
595
+ * gap the other neighbor would have drawn many times larger: a 16 Myr
596
+ * hiatus in column 22 read as 26px against one section and 166px against
597
+ * the other, and collapsed. A gap is only worth hiding when neither scale
598
+ * would give it more room than the break that replaces it. */
599
+ const pxHeight = Math.max(...pxHeights);
597
600
 
598
601
  if (pxHeight < threshold) {
599
602
  let t_pos: number;
@@ -0,0 +1,197 @@
1
+ /** How tall a column draws comes down to one quantity: **density**, the pixels
2
+ * given to one unit of the axis — a Myr on an age column, a metre otherwise.
3
+ *
4
+ * Every option that sets a column's height names that quantity. Most are
5
+ * floors, differing only in the units they're stated in: room for a unit,
6
+ * for a section, or for the density itself. They combine by taking the
7
+ * largest, so each says "at least this much", whichever ends up binding.
8
+ */
9
+ import { ColumnAxisType } from "@macrostrat/column-components";
10
+ import type { UnitLong } from "@macrostrat/api-types";
11
+
12
+ export interface SectionDensityContext {
13
+ /** The section's extent, in axis units */
14
+ extent: number;
15
+ /** Extents of the units the render window actually shows, each clipped to
16
+ * it. Measuring what's on screen is what makes a unit-height rule mean the
17
+ * same thing at every zoom depth. */
18
+ unitExtents: number[];
19
+ /** The section's units, unclipped, for a rule that needs more than their
20
+ * extents */
21
+ units: UnitLong[];
22
+ sectionID?: number;
23
+ axisType: ColumnAxisType;
24
+ }
25
+
26
+ /** Pixels per axis unit, for one section */
27
+ export type SectionDensity = (ctx: SectionDensityContext) => number;
28
+
29
+ export type SectionDensityLike = number | SectionDensity;
30
+
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);
43
+ }
44
+
45
+ /** Room for the section: `px` tall, whatever its extent. */
46
+ export function sectionHeight(px: number): SectionDensity {
47
+ return (ctx) => px / ctx.extent;
48
+ }
49
+
50
+ /** A density stated outright, in pixels per axis unit. */
51
+ export function fixedDensity(px: number): SectionDensity {
52
+ return () => px;
53
+ }
54
+
55
+ /** The rule that asks for the most room. Rules that don't resolve to a finite
56
+ * number — a section of no extent, a window showing no units — are passed
57
+ * over rather than swallowing the rest. */
58
+ export function atLeast(...rules: SectionDensityLike[]): SectionDensity {
59
+ return (ctx) => {
60
+ let density = 0;
61
+ for (const rule of rules) {
62
+ const value = resolveDensity(rule, ctx);
63
+ if (!Number.isFinite(value)) continue;
64
+ density = Math.max(density, value);
65
+ }
66
+ return density;
67
+ };
68
+ }
69
+
70
+ export function resolveDensity(
71
+ rule: SectionDensityLike,
72
+ ctx: SectionDensityContext,
73
+ ): number {
74
+ if (typeof rule === "number") return rule;
75
+ return rule(ctx);
76
+ }
77
+
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
+ /** Mirrors the default in `sectionDensity` */
88
+ export const DEFAULT_TARGET_UNIT_HEIGHT = 20;
89
+ /** Small sections often have a unit or two, and no room for axis labels */
90
+ const DEFAULT_MIN_PIXEL_SCALE = 0.2;
91
+
92
+ /** What a section is sized by. Stated for the column as a whole, and again
93
+ * per section by `sectionOptions` where one needs different treatment. */
94
+ export interface SectionSizeOptions {
95
+ /** A density stated outright, in pixels per axis unit: every section drawn
96
+ * to the same scale whatever it holds. Rarely what you want within one
97
+ * column, and the only way to make several of them comparable.
98
+ *
99
+ * Left out — the usual case — the density comes from the section's own
100
+ * units, via `targetUnitHeight` and the floors. Set, it *is* the density:
101
+ * the floors don't apply, since they would undo the scale you stated.
102
+ *
103
+ * It also takes a rule, for what the options here can't describe.
104
+ *
105
+ * This one is stated in whatever the axis measures, so it means a different
106
+ * thing on an age axis than on a depth one. A view that switches between
107
+ * them wants `pixelsPerMyr` and `pixelsPerMeter` instead. */
108
+ pixelScale?: SectionDensityLike;
109
+ /** Pixels per million years, on an age axis.
110
+ *
111
+ * The axis-specific spellings of `pixelScale`, and what to reach for when
112
+ * the axis can change under you: useful values differ by orders of
113
+ * magnitude between the two, so one number cannot serve both, any more than
114
+ * `t_age` could serve as `t_pos`. Both can be held at once — the axis picks
115
+ * — and either takes precedence over `pixelScale`. */
116
+ pixelsPerMyr?: number;
117
+ /** Pixels per metre, on a depth or height axis. Also the scale for an
118
+ * approximate-height column, whose units are metres of measured thickness
119
+ * however its axis is labeled. */
120
+ pixelsPerMeter?: number;
121
+ /** Room for a typical unit on screen. The knob that usually decides a
122
+ * column, and the one to reach for to draw it larger or smaller. */
123
+ targetUnitHeight?: number;
124
+ /** Room for a section, however little it holds */
125
+ minSectionHeight?: number;
126
+ /** A floor on the density itself */
127
+ minPixelScale?: number;
128
+ }
129
+
130
+ /** Sizing for one section, or a rule that works it out from what the section
131
+ * holds. What it returns overrides the column-wide options for that section;
132
+ * anything it leaves out keeps the column's value. */
133
+ export type SectionOptionsLike =
134
+ | SectionSizeOptions
135
+ | ((ctx: SectionDensityContext) => SectionSizeOptions);
136
+
137
+ export interface SectionDensityOptions extends SectionSizeOptions {
138
+ /** Sizing decided per section */
139
+ sectionOptions?: SectionOptionsLike;
140
+ }
141
+
142
+ /** The density rule a column uses, resolved per section so that
143
+ * `sectionOptions` gets to see a section before saying how to draw it. */
144
+ export function sectionDensity(opts: SectionDensityOptions): SectionDensity {
145
+ const { sectionOptions } = opts;
146
+ if (sectionOptions == null) {
147
+ const rule = densityRule(opts);
148
+ return (ctx) => rule(ctx);
149
+ }
150
+
151
+ return (ctx) => {
152
+ let overrides = sectionOptions;
153
+ if (typeof sectionOptions === "function") overrides = sectionOptions(ctx);
154
+ return densityRule({ ...opts, ...overrides })(ctx);
155
+ };
156
+ }
157
+
158
+ /** The floors, combined: whichever asks for the most room wins. */
159
+ function densityRule(opts: SectionSizeOptions): SectionDensity {
160
+ const {
161
+ targetUnitHeight = DEFAULT_TARGET_UNIT_HEIGHT,
162
+ minPixelScale = DEFAULT_MIN_PIXEL_SCALE,
163
+ minSectionHeight,
164
+ } = opts;
165
+
166
+ const derived = atLeast(
167
+ unitHeight(targetUnitHeight),
168
+ fixedDensity(minPixelScale),
169
+ sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0),
170
+ );
171
+
172
+ return (ctx) => {
173
+ // A density given outright answers for the section by itself. The floors
174
+ // exist to keep a column readable when its own contents decide the
175
+ // density; a scale you stated is a decision already made, and a floor
176
+ // would quietly undo it — which is the whole point of stating one,
177
+ // usually so that several columns can be compared.
178
+ const stated = statedDensity(opts, ctx.axisType);
179
+ if (typeof stated === "function") return stated(ctx);
180
+ if (stated != null) return stated;
181
+ return derived(ctx);
182
+ };
183
+ }
184
+
185
+ /** The density this axis was given, if any. The axis-specific spellings win
186
+ * over the generic one, which is what lets both be held while the axis
187
+ * changes. */
188
+ export function statedDensity(
189
+ opts: SectionSizeOptions,
190
+ axisType: ColumnAxisType,
191
+ ): SectionDensityLike | undefined {
192
+ const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;
193
+ if (axisType === ColumnAxisType.AGE) {
194
+ return pixelsPerMyr ?? pixelScale;
195
+ }
196
+ return pixelsPerMeter ?? pixelScale;
197
+ }
@@ -27,10 +27,11 @@ import {
27
27
  PreparedColumnData,
28
28
  unitsOverlap,
29
29
  } from "./utils";
30
- import { SectionInfo } from "./types";
30
+ import { type ColumnHeightScaleOptions, SectionInfo } from "./types";
31
31
 
32
32
  export * from "./utils";
33
33
  export * from "./types";
34
+ export * from "./density";
34
35
  export { preprocessUnits };
35
36
  export type { CompositeColumnScale };
36
37
 
@@ -92,14 +93,28 @@ export function prepareColumnUnits(
92
93
  // also set up some values for eODP-style columns
93
94
  let units1 = units.map(preprocessSectionUnit);
94
95
 
95
- if (clipBeforeLayout) {
96
+ /** A bound that isn't set doesn't bound anything. Passing `null` for one is
97
+ * the ordinary way to say "no window", and comparing against it as though
98
+ * it were an age would throw every unit out. */
99
+ const window = {
100
+ t_age: t_age ?? -Infinity,
101
+ b_age: b_age ?? Infinity,
102
+ t_pos: t_pos ?? -Infinity,
103
+ b_pos: b_pos ?? Infinity,
104
+ };
105
+ const isWindowed =
106
+ axisType == ColumnAxisType.AGE
107
+ ? t_age != null || b_age != null
108
+ : t_pos != null || b_pos != null;
109
+
110
+ if (clipBeforeLayout && isWindowed) {
96
111
  /** Prototype filtering to age range */
97
112
  units1 = units1.filter((d) => {
98
113
  // Filter units by t_age and b_age, inclusive
99
114
  if (axisType == ColumnAxisType.AGE) {
100
- return agesOverlap(d, { t_age, b_age });
115
+ return agesOverlap(d, window);
101
116
  } else {
102
- return unitsOverlap(d, { t_pos, b_pos } as any, axisType);
117
+ return unitsOverlap(d, window as any, axisType);
103
118
  }
104
119
  });
105
120
  }
@@ -207,11 +222,15 @@ export function prepareColumnUnits(
207
222
  // stretched to `minSectionHeight`), then spend the padding budget against
208
223
  // them, so a margin is the pixels asked for rather than those pixels times
209
224
  // whatever stretch its neighbor happened to need.
210
- const floor = options.minSectionHeight ?? options.targetUnitHeight ?? 0;
225
+ const floor =
226
+ layoutOptions.minSectionHeight ?? layoutOptions.targetUnitHeight ?? 0;
211
227
  const scales = resolveWindowScales(sectionsWithScales, focalWindow, floor);
212
- const scaleFor = (section) =>
228
+ const natural = (section) =>
213
229
  scales.get(section) ?? section.scaleInfo.pixelScale;
214
230
 
231
+
232
+ const scaleFor = natural;
233
+
215
234
  const window =
216
235
  windowPadding > 0
217
236
  ? padWindowByPixels(
@@ -1,24 +1,33 @@
1
1
  import type { UnitLong } from "@macrostrat/api-types";
2
2
  import type { ColumnAxisType } from "@macrostrat/column-components";
3
3
  import type { ScaleContinuousNumeric } from "d3-scale";
4
+ import type { SectionDensityLike, SectionOptionsLike } from "./density";
4
5
 
5
6
  export interface ColumnHeightScaleOptions {
6
- /** A fixed pixel scale to use for the section (pixels per Myr) */
7
- pixelScale?: number;
8
- /** The target height of a constituent unit in pixels, for dynamic
9
- * scale generation */
7
+ /** A density stated outright: pixels per Myr on an age column, per metre
8
+ * otherwise, every section drawn to the same scale whatever it holds.
9
+ * Rarely what you want within one column, and the only way to make several
10
+ * of them comparable.
11
+ *
12
+ * It also takes a rule — a function of the section's context — for the
13
+ * cases the options here can't describe. A rule is in sole charge of its
14
+ * section: the floors don't apply to it. */
15
+ pixelScale?: SectionDensityLike;
16
+ /** Room for a typical unit the render window shows: this many pixels tall,
17
+ * whatever its duration. The knob that usually decides a column — reach for
18
+ * it to draw one larger or smaller — and the one that expands it as you
19
+ * zoom in, since the units on screen keep their size while the time they
20
+ * cover shrinks. See `./density` for what "typical" means. */
10
21
  targetUnitHeight?: number;
11
- /** Min height of a section in pixels. Will override minPixelScale in some cases. */
22
+ /** Room for a section: at least this many pixels tall, whatever its extent.
23
+ * Small sections have a unit or two and little room for axis labels. */
12
24
  minSectionHeight?: number;
13
- /** The minimum pixel scale to use for the section (pixels per Myr). This is mostly
14
- * needed because small sections (<1-2 units) don't necessarily have space to comfortably
15
- * render two axis labels */
25
+ /** A floor on the density itself, in pixels per axis unit. */
16
26
  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];
27
+ /** Sizing for a section in particular, or a rule that works it out from
28
+ * what the section holds. Whatever it returns overrides the options above
29
+ * for that section; anything it leaves out keeps the column's value. */
30
+ sectionOptions?: SectionOptionsLike;
22
31
  /** Padding around the `t_age`/`b_age` window, in **pixels** of neighboring
23
32
  * column: how much of the abutting sections to reveal past the window.
24
33
  *
@@ -76,6 +85,13 @@ export type HybridScaleDefinition =
76
85
  export interface SectionScaleOptions extends ColumnHeightScaleOptions {
77
86
  axisType: ColumnAxisType;
78
87
  domain: [number, number];
88
+ /** Named in the context a per-section rule sees */
89
+ sectionID?: number;
90
+ /** The requested render window, `[b_age, t_age]`, if there is one. Set
91
+ * internally by `prepareColumnUnits`: unit density is derived from the units
92
+ * this window actually shows, so `targetUnitHeight` describes the units you
93
+ * can see at any zoom depth rather than the section's overall average. */
94
+ visibleWindow?: [number, number];
79
95
  }
80
96
 
81
97
  /** Output of a section scale. For now, this assumes that the
@@ -141,6 +157,11 @@ export interface CompositeScaleData {
141
157
  export interface ColumnScaleOptions extends ColumnHeightScaleOptions {
142
158
  axisType: ColumnAxisType;
143
159
  unconformityHeight: number;
160
+ /** The requested render window, `[b_age, t_age]`, if there is one. Set
161
+ * internally by `prepareColumnUnits`: unit density is derived from the units
162
+ * this window actually shows, so `targetUnitHeight` describes the units you
163
+ * can see at any zoom depth rather than the section's overall average. */
164
+ visibleWindow?: [number, number];
144
165
  }
145
166
 
146
167
  export interface CompositeColumnData<T extends UnitLong = ExtUnit> extends Omit<