@macrostrat/column-views 3.11.0 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/dist/age-axis.cjs +1 -2
- package/dist/age-axis.cjs.map +1 -1
- package/dist/age-axis.js +1 -2
- package/dist/age-axis.js.map +1 -1
- package/dist/age-model/data.cjs +57 -0
- package/dist/age-model/data.cjs.map +1 -0
- package/dist/age-model/data.d.ts +29 -0
- package/dist/age-model/data.js +57 -0
- package/dist/age-model/data.js.map +1 -0
- package/dist/age-model/details.cjs +260 -0
- package/dist/age-model/details.cjs.map +1 -0
- package/dist/age-model/details.d.ts +39 -0
- package/dist/age-model/details.js +257 -0
- package/dist/age-model/details.js.map +1 -0
- package/dist/age-model/details.module.sass.cjs +42 -0
- package/dist/age-model/details.module.sass.cjs.map +1 -0
- package/dist/age-model/details.module.sass.js +40 -0
- package/dist/age-model/details.module.sass.js.map +1 -0
- package/dist/age-model/index.d.ts +5 -0
- package/dist/age-model/legacy.cjs +28 -0
- package/dist/age-model/legacy.cjs.map +1 -0
- package/dist/age-model/legacy.d.ts +6 -0
- package/dist/age-model/legacy.js +26 -0
- package/dist/age-model/legacy.js.map +1 -0
- package/dist/age-model/surfaces.cjs +354 -0
- package/dist/age-model/surfaces.cjs.map +1 -0
- package/dist/age-model/surfaces.d.ts +68 -0
- package/dist/age-model/surfaces.js +351 -0
- package/dist/age-model/surfaces.js.map +1 -0
- package/dist/age-model/surfaces.module.sass.cjs +45 -0
- package/dist/age-model/surfaces.module.sass.cjs.map +1 -0
- package/dist/age-model/surfaces.module.sass.js +43 -0
- package/dist/age-model/surfaces.module.sass.js.map +1 -0
- package/dist/age-model/types.cjs +229 -0
- package/dist/age-model/types.cjs.map +1 -0
- package/dist/age-model/types.d.ts +112 -0
- package/dist/age-model/types.js +229 -0
- package/dist/age-model/types.js.map +1 -0
- package/dist/column-views.css +406 -74
- package/dist/column.cjs +20 -13
- package/dist/column.cjs.map +1 -1
- package/dist/column.d.ts +21 -6
- package/dist/column.js +21 -14
- package/dist/column.js.map +1 -1
- package/dist/column.module.sass.cjs +19 -15
- package/dist/column.module.sass.cjs.map +1 -1
- package/dist/column.module.sass.js +19 -15
- package/dist/column.module.sass.js.map +1 -1
- package/dist/data-provider/store.cjs +34 -1
- package/dist/data-provider/store.cjs.map +1 -1
- package/dist/data-provider/store.d.ts +5 -0
- package/dist/data-provider/store.js +35 -2
- package/dist/data-provider/store.js.map +1 -1
- package/dist/index.cjs +78 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +55 -4
- package/dist/index.js.map +1 -1
- package/dist/notes.cjs +11 -2
- package/dist/notes.cjs.map +1 -1
- package/dist/notes.d.ts +11 -1
- package/dist/notes.js +11 -2
- package/dist/notes.js.map +1 -1
- package/dist/prepare-units/composite-scale.cjs +18 -16
- package/dist/prepare-units/composite-scale.cjs.map +1 -1
- package/dist/prepare-units/composite-scale.js +18 -16
- package/dist/prepare-units/composite-scale.js.map +1 -1
- package/dist/prepare-units/density.cjs +83 -0
- package/dist/prepare-units/density.cjs.map +1 -0
- package/dist/prepare-units/density.d.ts +92 -0
- package/dist/prepare-units/density.js +83 -0
- package/dist/prepare-units/density.js.map +1 -0
- package/dist/prepare-units/index.cjs +15 -7
- package/dist/prepare-units/index.cjs.map +1 -1
- package/dist/prepare-units/index.d.ts +1 -0
- package/dist/prepare-units/index.js +15 -7
- package/dist/prepare-units/index.js.map +1 -1
- package/dist/prepare-units/types.cjs.map +1 -1
- package/dist/prepare-units/types.d.ts +34 -13
- package/dist/prepare-units/types.js.map +1 -1
- package/dist/section.cjs +108 -24
- package/dist/section.cjs.map +1 -1
- package/dist/section.d.ts +66 -6
- package/dist/section.js +109 -25
- package/dist/section.js.map +1 -1
- package/dist/timescale-zoom.cjs +191 -0
- package/dist/timescale-zoom.cjs.map +1 -0
- package/dist/timescale-zoom.d.ts +74 -0
- package/dist/timescale-zoom.js +191 -0
- package/dist/timescale-zoom.js.map +1 -0
- package/dist/unit-details/age-range.cjs +14 -43
- package/dist/unit-details/age-range.cjs.map +1 -1
- package/dist/unit-details/age-range.d.ts +1 -13
- package/dist/unit-details/age-range.js +6 -43
- package/dist/unit-details/age-range.js.map +1 -1
- package/dist/unit-details/utils.cjs +0 -5
- package/dist/unit-details/utils.cjs.map +1 -1
- package/dist/unit-details/utils.js +0 -5
- package/dist/unit-details/utils.js.map +1 -1
- package/dist/units/composite.cjs +4 -1
- package/dist/units/composite.cjs.map +1 -1
- package/dist/units/composite.d.ts +2 -0
- package/dist/units/composite.js +4 -1
- package/dist/units/composite.js.map +1 -1
- package/package.json +8 -8
- package/src/age-model/data.ts +98 -0
- package/src/age-model/details.module.sass +108 -0
- package/src/age-model/details.ts +333 -0
- package/src/age-model/index.ts +5 -0
- package/src/age-model/legacy.ts +30 -0
- package/src/age-model/surfaces.module.sass +168 -0
- package/src/age-model/surfaces.ts +568 -0
- package/src/age-model/types.ts +392 -0
- package/src/column.module.sass +59 -8
- package/src/column.ts +49 -23
- package/src/data-provider/store.ts +50 -1
- package/src/index.ts +5 -1
- package/src/notes.ts +20 -2
- package/src/prepare-units/composite-scale.ts +34 -31
- package/src/prepare-units/density.ts +197 -0
- package/src/prepare-units/index.ts +25 -6
- package/src/prepare-units/types.ts +34 -13
- package/src/section.ts +219 -33
- package/src/timescale-zoom.ts +341 -0
- package/src/unit-details/age-range.ts +12 -58
- package/src/units/composite.ts +11 -0
- package/dist/age-model-overlay.cjs +0 -50
- package/dist/age-model-overlay.cjs.map +0 -1
- package/dist/age-model-overlay.d.ts +0 -24
- package/dist/age-model-overlay.js +0 -48
- package/dist/age-model-overlay.js.map +0 -1
- package/dist/age-model-overlay.module.sass.cjs +0 -12
- package/dist/age-model-overlay.module.sass.cjs.map +0 -1
- package/dist/age-model-overlay.module.sass.js +0 -11
- package/dist/age-model-overlay.module.sass.js.map +0 -1
- package/src/age-model-overlay.module.sass +0 -11
- 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(
|
|
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
|
|
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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
|
430
|
+
function visibleUnitExtents(
|
|
433
431
|
data: UnitLong[],
|
|
434
432
|
axisType: ColumnAxisType,
|
|
435
433
|
visibleWindow?: [number, number] | null,
|
|
436
|
-
): number {
|
|
437
|
-
/** The
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
115
|
+
return agesOverlap(d, window);
|
|
101
116
|
} else {
|
|
102
|
-
return unitsOverlap(d,
|
|
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 =
|
|
225
|
+
const floor =
|
|
226
|
+
layoutOptions.minSectionHeight ?? layoutOptions.targetUnitHeight ?? 0;
|
|
211
227
|
const scales = resolveWindowScales(sectionsWithScales, focalWindow, floor);
|
|
212
|
-
const
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
|
|
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<
|