@macrostrat/column-views 3.6.1 → 3.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +144 -0
- package/dist/animated-age-window.cjs +51 -0
- package/dist/animated-age-window.cjs.map +1 -0
- package/dist/animated-age-window.d.ts +51 -0
- package/dist/animated-age-window.js +51 -0
- package/dist/animated-age-window.js.map +1 -0
- package/dist/column-views.css +118 -89
- package/dist/column.cjs +12 -3
- package/dist/column.cjs.map +1 -1
- package/dist/column.d.ts +11 -0
- package/dist/column.js +12 -3
- package/dist/column.js.map +1 -1
- package/dist/column.module.sass.cjs +15 -15
- package/dist/column.module.sass.js +15 -15
- package/dist/correlation-chart/main.cjs +156 -42
- package/dist/correlation-chart/main.cjs.map +1 -1
- package/dist/correlation-chart/main.d.ts +40 -2
- package/dist/correlation-chart/main.js +138 -23
- package/dist/correlation-chart/main.js.map +1 -1
- package/dist/correlation-chart/main.module.sass.cjs +23 -20
- package/dist/correlation-chart/main.module.sass.cjs.map +1 -1
- package/dist/correlation-chart/main.module.sass.js +23 -20
- package/dist/correlation-chart/main.module.sass.js.map +1 -1
- package/dist/correlation-chart/prepare-data.cjs +0 -1
- package/dist/correlation-chart/prepare-data.cjs.map +1 -1
- package/dist/correlation-chart/prepare-data.js +0 -1
- package/dist/correlation-chart/prepare-data.js.map +1 -1
- package/dist/data-provider/store.cjs +17 -4
- package/dist/data-provider/store.cjs.map +1 -1
- package/dist/data-provider/store.d.ts +6 -1
- package/dist/data-provider/store.js +17 -4
- package/dist/data-provider/store.js.map +1 -1
- package/dist/data-provider/unit-selection.cjs +4 -1
- package/dist/data-provider/unit-selection.cjs.map +1 -1
- package/dist/data-provider/unit-selection.d.ts +1 -1
- package/dist/data-provider/unit-selection.js +4 -1
- package/dist/data-provider/unit-selection.js.map +1 -1
- package/dist/index.cjs +16 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/maps/column-correlation/core.cjs +127 -6
- package/dist/maps/column-correlation/core.cjs.map +1 -1
- package/dist/maps/column-correlation/core.js +128 -7
- package/dist/maps/column-correlation/core.js.map +1 -1
- package/dist/maps/column-correlation/state.cjs +63 -2
- package/dist/maps/column-correlation/state.cjs.map +1 -1
- package/dist/maps/column-correlation/state.d.ts +31 -1
- package/dist/maps/column-correlation/state.js +63 -2
- package/dist/maps/column-correlation/state.js.map +1 -1
- package/dist/maps/layers/columns.cjs +5 -1
- package/dist/maps/layers/columns.cjs.map +1 -1
- package/dist/maps/layers/columns.js +5 -1
- package/dist/maps/layers/columns.js.map +1 -1
- package/dist/prepare-units/composite-scale.cjs +94 -8
- package/dist/prepare-units/composite-scale.cjs.map +1 -1
- package/dist/prepare-units/composite-scale.d.ts +39 -0
- package/dist/prepare-units/composite-scale.js +96 -10
- package/dist/prepare-units/composite-scale.js.map +1 -1
- package/dist/prepare-units/helpers.cjs +0 -1
- package/dist/prepare-units/helpers.cjs.map +1 -1
- package/dist/prepare-units/helpers.js +0 -1
- package/dist/prepare-units/helpers.js.map +1 -1
- package/dist/prepare-units/index.cjs +33 -12
- package/dist/prepare-units/index.cjs.map +1 -1
- package/dist/prepare-units/index.js +34 -13
- 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 +13 -0
- package/dist/prepare-units/types.js.map +1 -1
- package/dist/section.cjs +10 -2
- package/dist/section.cjs.map +1 -1
- package/dist/section.d.ts +5 -0
- package/dist/section.js +10 -2
- package/dist/section.js.map +1 -1
- package/dist/unit-details/age-range.cjs +2 -1
- package/dist/unit-details/age-range.cjs.map +1 -1
- package/dist/unit-details/age-range.js +2 -1
- package/dist/unit-details/age-range.js.map +1 -1
- package/dist/unit-details/index.d.ts +1 -1
- package/dist/unit-details/panel.cjs +2 -3
- package/dist/unit-details/panel.cjs.map +1 -1
- package/dist/unit-details/panel.js +1 -2
- package/dist/unit-details/panel.js.map +1 -1
- package/dist/unit-details/panel.module.sass.cjs +18 -22
- package/dist/unit-details/panel.module.sass.cjs.map +1 -1
- package/dist/unit-details/panel.module.sass.js +18 -22
- package/dist/unit-details/panel.module.sass.js.map +1 -1
- package/dist/unit-details/strat-names.cjs +1 -2
- package/dist/unit-details/strat-names.cjs.map +1 -1
- package/dist/unit-details/strat-names.js +1 -2
- package/dist/unit-details/strat-names.js.map +1 -1
- package/dist/units/boxes.cjs +20 -36
- package/dist/units/boxes.cjs.map +1 -1
- package/dist/units/boxes.js +22 -38
- package/dist/units/boxes.js.map +1 -1
- package/dist/units/composite.cjs +12 -12
- package/dist/units/composite.cjs.map +1 -1
- package/dist/units/composite.js +12 -12
- package/dist/units/composite.js.map +1 -1
- package/package.json +9 -9
- package/src/animated-age-window.ts +120 -0
- package/src/column.module.sass +6 -0
- package/src/column.ts +24 -0
- package/src/correlation-chart/main.module.sass +46 -2
- package/src/correlation-chart/main.ts +210 -17
- package/src/correlation-chart/prepare-data.ts +0 -1
- package/src/data-provider/store.ts +21 -2
- package/src/data-provider/unit-selection.ts +13 -2
- package/src/index.ts +1 -0
- package/src/maps/column-correlation/column-correlation-map.stories.ts +12 -0
- package/src/maps/column-correlation/core.ts +155 -9
- package/src/maps/column-correlation/state.ts +110 -2
- package/src/maps/layers/columns.ts +7 -1
- package/src/prepare-units/composite-scale.ts +210 -10
- package/src/prepare-units/helpers.ts +0 -2
- package/src/prepare-units/index.ts +68 -14
- package/src/prepare-units/types.ts +13 -0
- package/src/section.ts +15 -1
- package/src/unit-details/age-range.ts +3 -1
- package/src/unit-details/index.ts +7 -1
- package/src/unit-details/panel.module.sass +0 -8
- package/src/unit-details/panel.ts +6 -6
- package/src/unit-details/strat-names.ts +1 -1
- package/src/units/boxes.ts +33 -53
- package/src/units/composite.ts +21 -12
- package/dist/correlation-chart/stories/utils.d.ts +0 -6
- package/dist/unit-details/identifiers.cjs +0 -56
- package/dist/unit-details/identifiers.cjs.map +0 -1
- package/dist/unit-details/identifiers.d.ts +0 -31
- package/dist/unit-details/identifiers.js +0 -54
- package/dist/unit-details/identifiers.js.map +0 -1
- package/src/correlation-chart/stories/correlation-chart.stories.ts +0 -228
- package/src/correlation-chart/stories/namibia-correlation.stories.ts +0 -151
- package/src/correlation-chart/stories/stories.module.sass +0 -18
- package/src/correlation-chart/stories/utils.ts +0 -24
- package/src/unit-details/identifiers.ts +0 -67
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import { AgeDomain, useAnimatedDomain } from "@macrostrat/timescale";
|
|
3
|
+
|
|
4
|
+
/** A rendered age window, ordered as the column props expect:
|
|
5
|
+
* `t_age` is the younger (top) bound, `b_age` the older (bottom) bound. */
|
|
6
|
+
export interface AgeWindow {
|
|
7
|
+
t_age: number;
|
|
8
|
+
b_age: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AnimateWindowOptions {
|
|
12
|
+
duration?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseAnimatedAgeWindowOptions {
|
|
16
|
+
/** The full data extent — the window to reset to, and the initial window.
|
|
17
|
+
* `null` until the data (and hence extent) is known. */
|
|
18
|
+
fullExtent: AgeWindow | null;
|
|
19
|
+
/** Animation duration in ms (default 750). */
|
|
20
|
+
duration?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AnimatedAgeWindow {
|
|
24
|
+
/** The current animated window to feed to `t_age`/`b_age` column props
|
|
25
|
+
* (`null` until `fullExtent` is known). */
|
|
26
|
+
window: AgeWindow | null;
|
|
27
|
+
isAnimating: boolean;
|
|
28
|
+
/** True when the window is (approximately) the full extent. */
|
|
29
|
+
isFullExtent: boolean;
|
|
30
|
+
/** Pan-and-contract to an explicit window (clamped to the full extent).
|
|
31
|
+
* Density (`pixelScale`) is left untouched — this is not a zoom. */
|
|
32
|
+
zoomToWindow(target: AgeWindow, opts?: AnimateWindowOptions): void;
|
|
33
|
+
/** Pan-and-contract to a timescale interval, using `eag`/`lag`. */
|
|
34
|
+
zoomToInterval(
|
|
35
|
+
interval: { eag: number; lag: number },
|
|
36
|
+
opts?: AnimateWindowOptions,
|
|
37
|
+
): void;
|
|
38
|
+
/** Ease back to the full extent. */
|
|
39
|
+
reset(opts?: AnimateWindowOptions): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Animate the rendered age window (`t_age`/`b_age`) of a column or correlation
|
|
44
|
+
* chart. This is the "animate the current zoom approach" model: narrowing the
|
|
45
|
+
* window re-lays-out the column with explicit clipping — units past the bounds
|
|
46
|
+
* get a zig-zag edge, unconformities keep their fixed `unconformityHeight`, and
|
|
47
|
+
* `pixelScale` (density) is untouched, so a sub-interval click **pans and
|
|
48
|
+
* contracts** at constant density rather than zooming. Joint domain/range
|
|
49
|
+
* change (true zoom) stays under the user's direct control via `pixelScale`.
|
|
50
|
+
*
|
|
51
|
+
* The animation itself is `useAnimatedDomain` from `@macrostrat/timescale` —
|
|
52
|
+
* the same state machine that drives the timescale's `useZoomableScale`. This
|
|
53
|
+
* hook only maps an `[older, younger]` domain onto `{ b_age, t_age }`; it
|
|
54
|
+
* animates the window you ask for and nothing more. Revealing a margin of the
|
|
55
|
+
* neighboring column is the *layout's* job, via the `windowPadding` prop (px),
|
|
56
|
+
* which is resolved against real section heights rather than predicted here.
|
|
57
|
+
*/
|
|
58
|
+
export function useAnimatedAgeWindow(
|
|
59
|
+
options: UseAnimatedAgeWindowOptions,
|
|
60
|
+
): AnimatedAgeWindow {
|
|
61
|
+
const { fullExtent, duration } = options;
|
|
62
|
+
|
|
63
|
+
const extent = useMemo(() => toDomain(fullExtent), [
|
|
64
|
+
fullExtent?.t_age,
|
|
65
|
+
fullExtent?.b_age,
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
const anim = useAnimatedDomain({ extent, duration });
|
|
69
|
+
|
|
70
|
+
const zoomToWindow = useCallback(
|
|
71
|
+
(target: AgeWindow, opts: AnimateWindowOptions = {}) => {
|
|
72
|
+
anim.animateTo([target.b_age, target.t_age], {
|
|
73
|
+
duration: opts.duration,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
[anim.animateTo],
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const zoomToInterval = useCallback(
|
|
80
|
+
(
|
|
81
|
+
interval: { eag: number; lag: number },
|
|
82
|
+
opts: AnimateWindowOptions = {},
|
|
83
|
+
) => {
|
|
84
|
+
zoomToWindow({ t_age: interval.lag, b_age: interval.eag }, opts);
|
|
85
|
+
},
|
|
86
|
+
[zoomToWindow],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
const reset = useCallback(
|
|
90
|
+
(opts: AnimateWindowOptions = {}) => {
|
|
91
|
+
if (fullExtent == null) return;
|
|
92
|
+
zoomToWindow(fullExtent, opts);
|
|
93
|
+
},
|
|
94
|
+
[zoomToWindow, fullExtent?.t_age, fullExtent?.b_age],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
window: toWindow(anim.domain),
|
|
99
|
+
isAnimating: anim.isAnimating,
|
|
100
|
+
isFullExtent: anim.isFullExtent,
|
|
101
|
+
zoomToWindow,
|
|
102
|
+
zoomToInterval,
|
|
103
|
+
reset,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** `{t_age, b_age}` ↔ the core's `[older, younger]` domain. */
|
|
108
|
+
function toDomain(w: AgeWindow): AgeDomain;
|
|
109
|
+
function toDomain(w: AgeWindow | null): AgeDomain | null;
|
|
110
|
+
function toDomain(w: AgeWindow | null): AgeDomain | null {
|
|
111
|
+
if (w == null) return null;
|
|
112
|
+
return [w.b_age, w.t_age];
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function toWindow(d: AgeDomain): AgeWindow;
|
|
116
|
+
function toWindow(d: AgeDomain | null): AgeWindow | null;
|
|
117
|
+
function toWindow(d: AgeDomain | null): AgeWindow | null {
|
|
118
|
+
if (d == null) return null;
|
|
119
|
+
return { t_age: d[1], b_age: d[0] };
|
|
120
|
+
}
|
package/src/column.module.sass
CHANGED
|
@@ -57,6 +57,12 @@ body:global(.dark-mode) .column-container
|
|
|
57
57
|
fill: var(--selection-overlay-color, red)
|
|
58
58
|
fill-opacity: var(--selection-overlay-opacity, 0.5)
|
|
59
59
|
|
|
60
|
+
// Units sharing a stratigraphic name with the selection: a subtler highlight
|
|
61
|
+
.linked-overlay
|
|
62
|
+
fill: var(--linked-overlay-color, var(--selection-overlay-color, red))
|
|
63
|
+
fill-opacity: var(--linked-overlay-opacity, 0.18)
|
|
64
|
+
pointer-events: none
|
|
65
|
+
|
|
60
66
|
g.lithology-column use.frame
|
|
61
67
|
stroke: var(--column-stroke-color)
|
|
62
68
|
stroke-width: 1.5px !important
|
package/src/column.ts
CHANGED
|
@@ -45,6 +45,10 @@ import {
|
|
|
45
45
|
ColumnHeightScaleOptions,
|
|
46
46
|
} from "./prepare-units";
|
|
47
47
|
import { UnitLong } from "@macrostrat/api-types";
|
|
48
|
+
import type {
|
|
49
|
+
TimescaleClickHandler,
|
|
50
|
+
IntervalStyleBuilder,
|
|
51
|
+
} from "@macrostrat/timescale";
|
|
48
52
|
import { NonIdealState } from "@blueprintjs/core";
|
|
49
53
|
import { DataField } from "@macrostrat/data-components";
|
|
50
54
|
import { ScaleContinuousNumeric } from "d3-scale";
|
|
@@ -61,6 +65,10 @@ interface BaseColumnProps extends Omit<
|
|
|
61
65
|
// Timescale properties
|
|
62
66
|
showTimescale?: boolean;
|
|
63
67
|
timescaleLevels?: number | [number, number];
|
|
68
|
+
/** Called when a timescale interval is clicked (e.g. to zoom the age range). */
|
|
69
|
+
onClickTimescaleInterval?: TimescaleClickHandler;
|
|
70
|
+
/** Per-interval style for the timescale (e.g. to bold the selected interval). */
|
|
71
|
+
timescaleIntervalStyle?: IntervalStyleBuilder;
|
|
64
72
|
unconformityLabels?: boolean | UnconformityLabelPlacement;
|
|
65
73
|
onMouseOver?: (
|
|
66
74
|
unit: UnitLong | null,
|
|
@@ -87,6 +95,12 @@ export interface ColumnProps
|
|
|
87
95
|
// Unconformity height in pixels
|
|
88
96
|
unconformityHeight?: number;
|
|
89
97
|
scale?: ScaleContinuousNumeric<number, number>;
|
|
98
|
+
/** True while the age window is animating (e.g. from `useAnimatedAgeWindow`),
|
|
99
|
+
* so per-frame label/pattern recalculation can be skipped. */
|
|
100
|
+
isTransitioning?: boolean;
|
|
101
|
+
/** Hide unit labels while transitioning (perf escape hatch; default false —
|
|
102
|
+
* labels stay visible through the animation). */
|
|
103
|
+
hideLabelsWhileTransitioning?: boolean;
|
|
90
104
|
ref?: RefObject<ColumnRef>;
|
|
91
105
|
}
|
|
92
106
|
|
|
@@ -108,11 +122,14 @@ export function Column(props: ColumnProps) {
|
|
|
108
122
|
pixelScale,
|
|
109
123
|
minPixelScale = 0.2,
|
|
110
124
|
minSectionHeight = 50,
|
|
125
|
+
windowPadding = 0,
|
|
111
126
|
collapseSmallUnconformities = true,
|
|
112
127
|
allowUnitSelection = false,
|
|
113
128
|
hybridScale,
|
|
114
129
|
scale,
|
|
115
130
|
axisType,
|
|
131
|
+
isTransitioning,
|
|
132
|
+
hideLabelsWhileTransitioning,
|
|
116
133
|
ref,
|
|
117
134
|
...rest
|
|
118
135
|
} = props;
|
|
@@ -152,6 +169,7 @@ export function Column(props: ColumnProps) {
|
|
|
152
169
|
pixelScale,
|
|
153
170
|
minPixelScale: _minPixelScale,
|
|
154
171
|
minSectionHeight: _minSectionHeight,
|
|
172
|
+
windowPadding,
|
|
155
173
|
collapseSmallUnconformities,
|
|
156
174
|
// TODO: consider unifying scale and hybridScale options
|
|
157
175
|
scale,
|
|
@@ -179,6 +197,8 @@ export function Column(props: ColumnProps) {
|
|
|
179
197
|
allowUnitSelection: showUnitPopover || allowUnitSelection,
|
|
180
198
|
onUnitSelected,
|
|
181
199
|
selectedUnit,
|
|
200
|
+
isTransitioning,
|
|
201
|
+
hideLabelsWhileTransitioning,
|
|
182
202
|
ref,
|
|
183
203
|
},
|
|
184
204
|
h(ColumnInner, { ageAxisComponent, ...rest }, [
|
|
@@ -218,6 +238,8 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
218
238
|
timescaleLevels,
|
|
219
239
|
maxInternalColumns,
|
|
220
240
|
onMouseOver,
|
|
241
|
+
onClickTimescaleInterval,
|
|
242
|
+
timescaleIntervalStyle,
|
|
221
243
|
ageAxisComponent = CompositeAgeAxis,
|
|
222
244
|
} = props;
|
|
223
245
|
|
|
@@ -262,6 +284,8 @@ function ColumnInner(props: ColumnInnerProps) {
|
|
|
262
284
|
h.if(_showTimescale)(CompositeTimescale, {
|
|
263
285
|
levels: timescaleLevels,
|
|
264
286
|
unconformityLabels: _timescaleUnconformityLabels,
|
|
287
|
+
onClickInterval: onClickTimescaleInterval,
|
|
288
|
+
intervalStyle: timescaleIntervalStyle,
|
|
265
289
|
}),
|
|
266
290
|
h(SectionsColumn, {
|
|
267
291
|
unitComponent,
|
|
@@ -105,11 +105,16 @@ $map-size-inset: 12em
|
|
|
105
105
|
|
|
106
106
|
.main-chart
|
|
107
107
|
position: relative
|
|
108
|
+
grid-column: 2
|
|
109
|
+
grid-row: 2
|
|
108
110
|
|
|
109
111
|
.correlation-chart-inner
|
|
110
|
-
display:
|
|
111
|
-
|
|
112
|
+
display: grid
|
|
113
|
+
// [age axis] [columns], with an optional header row above
|
|
114
|
+
grid-template-columns: auto auto
|
|
115
|
+
grid-template-rows: auto auto
|
|
112
116
|
position: relative
|
|
117
|
+
width: fit-content
|
|
113
118
|
|
|
114
119
|
.age-axis-container
|
|
115
120
|
display: flex
|
|
@@ -117,6 +122,45 @@ $map-size-inset: 12em
|
|
|
117
122
|
position: sticky
|
|
118
123
|
left: 0
|
|
119
124
|
z-index: 100
|
|
125
|
+
grid-column: 1
|
|
126
|
+
grid-row: 2
|
|
120
127
|
padding-left: 1em
|
|
121
128
|
padding-right: 2px
|
|
122
129
|
background-color: var(--panel-secondary-background-color)
|
|
130
|
+
|
|
131
|
+
// Arbitrary content rendered above each column, aligned with the columns below.
|
|
132
|
+
.column-header-spacer
|
|
133
|
+
grid-column: 1
|
|
134
|
+
grid-row: 1
|
|
135
|
+
// Anchor for absolutely-positioned content (e.g. a zoom pill) so it can sit
|
|
136
|
+
// above the timescale without widening the axis column / shifting the chart
|
|
137
|
+
position: relative
|
|
138
|
+
|
|
139
|
+
.column-header-row
|
|
140
|
+
grid-column: 2
|
|
141
|
+
grid-row: 1
|
|
142
|
+
display: flex
|
|
143
|
+
flex-direction: row
|
|
144
|
+
// Bottom-anchor cells so all labels share the neatline
|
|
145
|
+
align-items: flex-end
|
|
146
|
+
// Pin to the top while scrolling. It stays in flow (grid row 1) so it
|
|
147
|
+
// reserves space above the columns, keeping them aligned with the age axis.
|
|
148
|
+
position: sticky
|
|
149
|
+
top: 0
|
|
150
|
+
// `position: sticky` forms a stacking context; leaving z-index at auto keeps
|
|
151
|
+
// the row below later-painted floating UI (maps, popovers).
|
|
152
|
+
// Neatline separating the headers from the columns below
|
|
153
|
+
border-bottom: 1px solid var(--column-stroke-color, var(--text-color))
|
|
154
|
+
background-color: var(--panel-secondary-background-color)
|
|
155
|
+
|
|
156
|
+
.column-header-cell
|
|
157
|
+
box-sizing: border-box
|
|
158
|
+
display: flex
|
|
159
|
+
flex-direction: column
|
|
160
|
+
// Bottom-anchor content (nearest the columns), stretched to the column
|
|
161
|
+
// width so text wraps within it
|
|
162
|
+
justify-content: flex-end
|
|
163
|
+
align-items: stretch
|
|
164
|
+
// Cap very long content; excess is clipped
|
|
165
|
+
max-height: var(--column-header-max-height, 300px)
|
|
166
|
+
overflow: hidden
|
|
@@ -6,8 +6,7 @@ import {
|
|
|
6
6
|
useColumnRef,
|
|
7
7
|
} from "../data-provider";
|
|
8
8
|
import { UnitDetailsFeature, UnitSelectionPopover } from "../unit-details";
|
|
9
|
-
import
|
|
10
|
-
import styles from "./main.module.sass";
|
|
9
|
+
import h from "./main.module.sass";
|
|
11
10
|
import { useMemo } from "react";
|
|
12
11
|
import { useInDarkMode } from "@macrostrat/ui-components";
|
|
13
12
|
import { CompositeTimescaleCore } from "../section";
|
|
@@ -31,12 +30,30 @@ import {
|
|
|
31
30
|
import { ColoredUnitComponent } from "../units";
|
|
32
31
|
import { UnitBoxes } from "../units/boxes";
|
|
33
32
|
import { ColumnContainer } from "../column";
|
|
34
|
-
import { ColumnData } from "
|
|
33
|
+
import type { ColumnData } from "@macrostrat/data-provider";
|
|
35
34
|
import { BaseUnit } from "@macrostrat/api-types";
|
|
36
35
|
import { ScaleContinuousNumeric } from "d3-scale";
|
|
37
36
|
import { ExtUnit } from "../prepare-units/types";
|
|
37
|
+
import type { TimescaleClickHandler } from "@macrostrat/timescale";
|
|
38
|
+
import { useMacrostratColumnInfo } from "@macrostrat/data-provider";
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
/** Standard props passed to a `columnHeaderComponent`. Everything a header
|
|
41
|
+
* needs is provided here, so the component can be a pure function of its props
|
|
42
|
+
* (no hooks required). Column-level hover/click are handled by the chart. */
|
|
43
|
+
export interface ColumnHeaderProps {
|
|
44
|
+
/** The column data (units + identifier) for this column */
|
|
45
|
+
column: ColumnData;
|
|
46
|
+
/** The column's ID (col_id) */
|
|
47
|
+
columnID: number;
|
|
48
|
+
/** The column's index (left-to-right) within the chart */
|
|
49
|
+
columnIndex: number;
|
|
50
|
+
/** The units within the column */
|
|
51
|
+
units: ExtUnit[];
|
|
52
|
+
/** The rendered width of the column, in pixels */
|
|
53
|
+
width: number;
|
|
54
|
+
/** The column's name, resolved from column metadata when available */
|
|
55
|
+
columnName?: string | null;
|
|
56
|
+
}
|
|
40
57
|
|
|
41
58
|
export interface CorrelationChartProps extends CorrelationChartSettings {
|
|
42
59
|
data: ColumnData[];
|
|
@@ -47,9 +64,40 @@ export interface CorrelationChartProps extends CorrelationChartSettings {
|
|
|
47
64
|
selectedUnit?: number | null;
|
|
48
65
|
showUnitPopover?: boolean;
|
|
49
66
|
unitComponent?: any;
|
|
67
|
+
/** Arbitrary content (e.g. column title and/or ID) rendered above each column */
|
|
68
|
+
columnHeaderComponent?: React.ComponentType<ColumnHeaderProps>;
|
|
69
|
+
/** Content rendered above the timescale axis (top-left), beside the column
|
|
70
|
+
* headers — e.g. a zoom/filter indicator. */
|
|
71
|
+
axisTopContent?: React.ReactNode;
|
|
72
|
+
/** True while the age window is animating (e.g. from `useAnimatedAgeWindow`).
|
|
73
|
+
* Propagated to each column's context so per-frame label/pattern work can be
|
|
74
|
+
* skipped during the transition. */
|
|
75
|
+
isTransitioning?: boolean;
|
|
76
|
+
/** Hide unit labels while transitioning (perf escape hatch; default false —
|
|
77
|
+
* labels stay visible through the animation). */
|
|
78
|
+
hideLabelsWhileTransitioning?: boolean;
|
|
50
79
|
onUnitSelected?: (unitID: number | null, unit: BaseUnit | null) => void;
|
|
80
|
+
/** Called when a timescale interval is clicked (e.g. to zoom the age range) */
|
|
81
|
+
onClickTimescaleInterval?: TimescaleClickHandler;
|
|
82
|
+
/** Called when the pointer enters/leaves a column (header or body). Passes
|
|
83
|
+
* the column ID, or null on leave. */
|
|
84
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
85
|
+
/** Called when a column header is clicked (e.g. to frame it on a map) */
|
|
86
|
+
onColumnClick?: (columnID: number) => void;
|
|
51
87
|
}
|
|
52
88
|
|
|
89
|
+
/** Horizontal padding of the main chart SVG. Column headers are aligned to
|
|
90
|
+
* this so that they line up with the columns beneath them. */
|
|
91
|
+
const chartPaddingH = 4;
|
|
92
|
+
|
|
93
|
+
/** Default scale settings for the correlation chart. */
|
|
94
|
+
const defaultCorrelationChartScaleProps = {
|
|
95
|
+
targetUnitHeight: 10,
|
|
96
|
+
unconformityHeight: 60,
|
|
97
|
+
minSectionHeight: 60,
|
|
98
|
+
collapseSmallUnconformities: true,
|
|
99
|
+
};
|
|
100
|
+
|
|
53
101
|
function MainChartArea({ children }) {
|
|
54
102
|
const columnRef = useColumnRef();
|
|
55
103
|
return h("div.main-chart", { ref: columnRef }, children);
|
|
@@ -71,19 +119,19 @@ export function CorrelationChart({
|
|
|
71
119
|
selectedUnit,
|
|
72
120
|
onUnitSelected,
|
|
73
121
|
unitComponent,
|
|
122
|
+
columnHeaderComponent,
|
|
123
|
+
axisTopContent,
|
|
124
|
+
onClickTimescaleInterval,
|
|
125
|
+
onColumnMouseOver,
|
|
126
|
+
onColumnClick,
|
|
127
|
+
isTransitioning = false,
|
|
128
|
+
hideLabelsWhileTransitioning = false,
|
|
74
129
|
...scaleProps
|
|
75
130
|
}: CorrelationChartProps) {
|
|
76
|
-
const defaultScaleProps = {
|
|
77
|
-
targetUnitHeight: 10,
|
|
78
|
-
unconformityHeight: 60,
|
|
79
|
-
minSectionHeight: 60,
|
|
80
|
-
collapseSmallUnconformities: true,
|
|
81
|
-
};
|
|
82
|
-
|
|
83
131
|
const chartData = useMemo(() => {
|
|
84
132
|
if (!data) return null;
|
|
85
133
|
return buildCorrelationChartData(data, {
|
|
86
|
-
...
|
|
134
|
+
...defaultCorrelationChartScaleProps,
|
|
87
135
|
...scaleProps,
|
|
88
136
|
});
|
|
89
137
|
}, [data, ...Object.values(scaleProps)]);
|
|
@@ -115,9 +163,9 @@ export function CorrelationChart({
|
|
|
115
163
|
{ selectedUnit, onUnitSelected, units },
|
|
116
164
|
h(ChartArea, [
|
|
117
165
|
h(TimescaleColumn, {
|
|
118
|
-
key: "timescale",
|
|
119
166
|
scaleInfo,
|
|
120
167
|
unconformityLabels,
|
|
168
|
+
onClickInterval: onClickTimescaleInterval,
|
|
121
169
|
}),
|
|
122
170
|
h(MainChartArea, [
|
|
123
171
|
h(
|
|
@@ -126,7 +174,7 @@ export function CorrelationChart({
|
|
|
126
174
|
className,
|
|
127
175
|
innerWidth: mainWidth,
|
|
128
176
|
height: scaleInfo.totalHeight,
|
|
129
|
-
paddingH:
|
|
177
|
+
paddingH: chartPaddingH,
|
|
130
178
|
},
|
|
131
179
|
packages.map((pkg, i) => {
|
|
132
180
|
const { offset, domain, pixelScale, scale, key } =
|
|
@@ -141,6 +189,9 @@ export function CorrelationChart({
|
|
|
141
189
|
pixelScale,
|
|
142
190
|
scale,
|
|
143
191
|
unitComponent,
|
|
192
|
+
onColumnMouseOver,
|
|
193
|
+
isTransitioning,
|
|
194
|
+
hideLabelsWhileTransitioning,
|
|
144
195
|
});
|
|
145
196
|
}),
|
|
146
197
|
),
|
|
@@ -150,6 +201,17 @@ export function CorrelationChart({
|
|
|
150
201
|
// Navigation only works within a column for now...
|
|
151
202
|
h(UnitKeyboardNavigation, { columnData: data }),
|
|
152
203
|
]),
|
|
204
|
+
// Rendered last so the sticky header paints above the unit boxes
|
|
205
|
+
// (positioned siblings paint in document order at the same z-index)
|
|
206
|
+
h(ColumnHeaderRow, {
|
|
207
|
+
data,
|
|
208
|
+
columnWidth,
|
|
209
|
+
columnSpacing,
|
|
210
|
+
columnHeaderComponent,
|
|
211
|
+
axisTopContent,
|
|
212
|
+
onColumnMouseOver,
|
|
213
|
+
onColumnClick,
|
|
214
|
+
}),
|
|
153
215
|
]),
|
|
154
216
|
),
|
|
155
217
|
);
|
|
@@ -164,6 +226,9 @@ function Package({
|
|
|
164
226
|
domain,
|
|
165
227
|
pixelScale,
|
|
166
228
|
scale,
|
|
229
|
+
onColumnMouseOver,
|
|
230
|
+
isTransitioning,
|
|
231
|
+
hideLabelsWhileTransitioning,
|
|
167
232
|
}) {
|
|
168
233
|
return h("g.package", { transform: `translate(0 ${offset})` }, [
|
|
169
234
|
// Disable the SVG overlay for now
|
|
@@ -172,6 +237,7 @@ function Package({
|
|
|
172
237
|
columnData.map((data, i) => {
|
|
173
238
|
return h(Column, {
|
|
174
239
|
units: data.units,
|
|
240
|
+
columnID: data.columnID,
|
|
175
241
|
unitComponent,
|
|
176
242
|
width: columnWidth,
|
|
177
243
|
key: i,
|
|
@@ -179,6 +245,9 @@ function Package({
|
|
|
179
245
|
pixelScale,
|
|
180
246
|
scale,
|
|
181
247
|
offsetLeft: i * (columnWidth + columnSpacing),
|
|
248
|
+
onColumnMouseOver,
|
|
249
|
+
isTransitioning,
|
|
250
|
+
hideLabelsWhileTransitioning,
|
|
182
251
|
});
|
|
183
252
|
}),
|
|
184
253
|
]),
|
|
@@ -187,6 +256,7 @@ function Package({
|
|
|
187
256
|
|
|
188
257
|
interface ColumnProps {
|
|
189
258
|
units: ExtUnit[];
|
|
259
|
+
columnID?: number;
|
|
190
260
|
unitComponent?: React.FunctionComponent<any>;
|
|
191
261
|
unitComponentProps?: any;
|
|
192
262
|
showLabels?: boolean;
|
|
@@ -198,17 +268,24 @@ interface ColumnProps {
|
|
|
198
268
|
domain: [number, number];
|
|
199
269
|
pixelScale: number;
|
|
200
270
|
scale?: ScaleContinuousNumeric<number, number>;
|
|
271
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
272
|
+
isTransitioning?: boolean;
|
|
273
|
+
hideLabelsWhileTransitioning?: boolean;
|
|
201
274
|
}
|
|
202
275
|
|
|
203
276
|
function Column(props: ColumnProps) {
|
|
204
277
|
const {
|
|
205
278
|
units,
|
|
279
|
+
columnID,
|
|
206
280
|
width = 150,
|
|
207
281
|
offsetLeft,
|
|
208
282
|
domain,
|
|
209
283
|
pixelScale,
|
|
210
284
|
scale,
|
|
211
285
|
unitComponent = ColoredUnitComponent,
|
|
286
|
+
onColumnMouseOver,
|
|
287
|
+
isTransitioning,
|
|
288
|
+
hideLabelsWhileTransitioning,
|
|
212
289
|
} = props;
|
|
213
290
|
|
|
214
291
|
const columnWidth = width;
|
|
@@ -217,10 +294,19 @@ function Column(props: ColumnProps) {
|
|
|
217
294
|
return null;
|
|
218
295
|
}
|
|
219
296
|
|
|
297
|
+
const hoverHandlers =
|
|
298
|
+
onColumnMouseOver != null && columnID != null
|
|
299
|
+
? {
|
|
300
|
+
onMouseEnter: () => onColumnMouseOver(columnID),
|
|
301
|
+
onMouseLeave: () => onColumnMouseOver(null),
|
|
302
|
+
}
|
|
303
|
+
: {};
|
|
304
|
+
|
|
220
305
|
return h(
|
|
221
306
|
"g.section",
|
|
222
307
|
{
|
|
223
308
|
transform: `translate(${offsetLeft} 0)`,
|
|
309
|
+
...hoverHandlers,
|
|
224
310
|
},
|
|
225
311
|
h(
|
|
226
312
|
ColumnProvider,
|
|
@@ -231,11 +317,12 @@ function Column(props: ColumnProps) {
|
|
|
231
317
|
scale,
|
|
232
318
|
pixelsPerMeter: pixelScale, // Actually pixels per myr
|
|
233
319
|
axisType: ColumnAxisType.AGE,
|
|
320
|
+
isTransitioning,
|
|
321
|
+
hideLabelsWhileTransitioning,
|
|
234
322
|
},
|
|
235
323
|
h(UnitBoxes, {
|
|
236
324
|
unitComponent,
|
|
237
325
|
unitComponentProps: {
|
|
238
|
-
nColumns: 2,
|
|
239
326
|
width: columnWidth,
|
|
240
327
|
showLabel: true,
|
|
241
328
|
},
|
|
@@ -322,16 +409,122 @@ function ChartArea({ children }) {
|
|
|
322
409
|
);
|
|
323
410
|
}
|
|
324
411
|
|
|
412
|
+
interface ColumnHeaderRowProps {
|
|
413
|
+
data: ColumnData[];
|
|
414
|
+
columnWidth: number;
|
|
415
|
+
columnSpacing: number;
|
|
416
|
+
columnHeaderComponent?: React.ComponentType<ColumnHeaderProps>;
|
|
417
|
+
axisTopContent?: React.ReactNode;
|
|
418
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
419
|
+
onColumnClick?: (columnID: number) => void;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function ColumnHeaderRow({
|
|
423
|
+
data,
|
|
424
|
+
columnWidth,
|
|
425
|
+
columnSpacing,
|
|
426
|
+
columnHeaderComponent,
|
|
427
|
+
axisTopContent,
|
|
428
|
+
onColumnMouseOver,
|
|
429
|
+
onColumnClick,
|
|
430
|
+
}: ColumnHeaderRowProps) {
|
|
431
|
+
/** The top grid row: arbitrary content above the timescale axis (top-left)
|
|
432
|
+
* and, when a header component is provided, per-column headers aligned with
|
|
433
|
+
* the columns. Collapses to nothing when neither is present. */
|
|
434
|
+
const Component = columnHeaderComponent;
|
|
435
|
+
if (Component == null && axisTopContent == null) {
|
|
436
|
+
return null;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return h([
|
|
440
|
+
h("div.column-header-spacer", axisTopContent),
|
|
441
|
+
h.if(Component != null)(
|
|
442
|
+
"div.column-header-row",
|
|
443
|
+
{
|
|
444
|
+
style: {
|
|
445
|
+
paddingLeft: chartPaddingH,
|
|
446
|
+
paddingRight: chartPaddingH,
|
|
447
|
+
gap: columnSpacing,
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
data.map((column, i) => {
|
|
451
|
+
return h(ColumnHeaderCell, {
|
|
452
|
+
key: column.columnID ?? i,
|
|
453
|
+
column,
|
|
454
|
+
columnIndex: i,
|
|
455
|
+
columnWidth,
|
|
456
|
+
Component: Component!,
|
|
457
|
+
onColumnMouseOver,
|
|
458
|
+
onColumnClick,
|
|
459
|
+
});
|
|
460
|
+
}),
|
|
461
|
+
),
|
|
462
|
+
]);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function ColumnHeaderCell({
|
|
466
|
+
column,
|
|
467
|
+
columnIndex,
|
|
468
|
+
columnWidth,
|
|
469
|
+
Component,
|
|
470
|
+
onColumnMouseOver,
|
|
471
|
+
onColumnClick,
|
|
472
|
+
}: {
|
|
473
|
+
column: ColumnData;
|
|
474
|
+
columnIndex: number;
|
|
475
|
+
columnWidth: number;
|
|
476
|
+
Component: React.ComponentType<ColumnHeaderProps>;
|
|
477
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
478
|
+
onColumnClick?: (columnID: number) => void;
|
|
479
|
+
}) {
|
|
480
|
+
/** Resolves the column name and wires column-level hover/click so the header
|
|
481
|
+
* component itself can stay a pure function of props. */
|
|
482
|
+
const columnID = column.columnID;
|
|
483
|
+
const info = useMacrostratColumnInfo(columnID);
|
|
484
|
+
|
|
485
|
+
return h(
|
|
486
|
+
"div.column-header-cell",
|
|
487
|
+
{
|
|
488
|
+
style: {
|
|
489
|
+
width: columnWidth,
|
|
490
|
+
minWidth: columnWidth,
|
|
491
|
+
maxWidth: columnWidth,
|
|
492
|
+
},
|
|
493
|
+
onMouseEnter: () => onColumnMouseOver?.(columnID),
|
|
494
|
+
onMouseLeave: () => onColumnMouseOver?.(null),
|
|
495
|
+
onClick: onColumnClick
|
|
496
|
+
? (e: React.MouseEvent) => {
|
|
497
|
+
e.stopPropagation();
|
|
498
|
+
onColumnClick(columnID);
|
|
499
|
+
}
|
|
500
|
+
: undefined,
|
|
501
|
+
},
|
|
502
|
+
h(Component, {
|
|
503
|
+
column,
|
|
504
|
+
columnID,
|
|
505
|
+
columnIndex,
|
|
506
|
+
units: column.units as ExtUnit[],
|
|
507
|
+
width: columnWidth,
|
|
508
|
+
columnName: info?.col_name ?? null,
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
|
|
325
513
|
interface TimescaleColumnProps {
|
|
326
514
|
scaleInfo: CompositeStratigraphicScaleInfo;
|
|
327
515
|
showLabels?: boolean;
|
|
328
516
|
unconformityLabels?: boolean;
|
|
517
|
+
onClickInterval?: TimescaleClickHandler;
|
|
329
518
|
}
|
|
330
519
|
|
|
331
520
|
function TimescaleColumn(props: TimescaleColumnProps) {
|
|
332
|
-
const { scaleInfo, unconformityLabels = true } = props;
|
|
521
|
+
const { scaleInfo, unconformityLabels = true, onClickInterval } = props;
|
|
333
522
|
return h("div.column-container.age-axis-container", [
|
|
334
523
|
h(CompositeAgeAxisCore, { ...scaleInfo }),
|
|
335
|
-
h(CompositeTimescaleCore, {
|
|
524
|
+
h(CompositeTimescaleCore, {
|
|
525
|
+
...scaleInfo,
|
|
526
|
+
unconformityLabels,
|
|
527
|
+
onClickInterval,
|
|
528
|
+
}),
|
|
336
529
|
]);
|
|
337
530
|
}
|