@macrostrat/column-views 3.6.0 → 3.8.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 +50 -0
- package/dist/column-views.css +118 -89
- package/dist/column.cjs +1 -2
- package/dist/column.cjs.map +1 -1
- package/dist/column.js +1 -2
- 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 +136 -34
- package/dist/correlation-chart/main.cjs.map +1 -1
- package/dist/correlation-chart/main.d.ts +33 -2
- package/dist/correlation-chart/main.js +118 -15
- 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 +10 -2
- package/dist/data-provider/store.cjs.map +1 -1
- package/dist/data-provider/store.d.ts +2 -2
- package/dist/data-provider/store.js +10 -2
- 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 +14 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -2
- 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/helpers.cjs +4 -2
- package/dist/prepare-units/helpers.cjs.map +1 -1
- package/dist/prepare-units/helpers.js +4 -2
- package/dist/prepare-units/helpers.js.map +1 -1
- package/dist/section.cjs +8 -2
- package/dist/section.cjs.map +1 -1
- package/dist/section.d.ts +2 -0
- package/dist/section.js +8 -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 +2 -1
- package/dist/units/boxes.cjs.map +1 -1
- package/dist/units/boxes.js +2 -1
- package/dist/units/boxes.js.map +1 -1
- package/package.json +7 -7
- package/src/column.module.sass +6 -0
- package/src/correlation-chart/main.module.sass +46 -2
- package/src/correlation-chart/main.ts +180 -9
- package/src/correlation-chart/prepare-data.ts +0 -1
- package/src/data-provider/store.ts +11 -1
- package/src/data-provider/unit-selection.ts +13 -2
- 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/helpers.ts +4 -3
- package/src/section.ts +9 -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 +4 -2
- 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
|
@@ -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,25 @@ 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;
|
|
50
72
|
onUnitSelected?: (unitID: number | null, unit: BaseUnit | null) => void;
|
|
73
|
+
/** Called when a timescale interval is clicked (e.g. to zoom the age range) */
|
|
74
|
+
onClickTimescaleInterval?: TimescaleClickHandler;
|
|
75
|
+
/** Called when the pointer enters/leaves a column (header or body). Passes
|
|
76
|
+
* the column ID, or null on leave. */
|
|
77
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
78
|
+
/** Called when a column header is clicked (e.g. to frame it on a map) */
|
|
79
|
+
onColumnClick?: (columnID: number) => void;
|
|
51
80
|
}
|
|
52
81
|
|
|
82
|
+
/** Horizontal padding of the main chart SVG. Column headers are aligned to
|
|
83
|
+
* this so that they line up with the columns beneath them. */
|
|
84
|
+
const chartPaddingH = 4;
|
|
85
|
+
|
|
53
86
|
function MainChartArea({ children }) {
|
|
54
87
|
const columnRef = useColumnRef();
|
|
55
88
|
return h("div.main-chart", { ref: columnRef }, children);
|
|
@@ -71,6 +104,11 @@ export function CorrelationChart({
|
|
|
71
104
|
selectedUnit,
|
|
72
105
|
onUnitSelected,
|
|
73
106
|
unitComponent,
|
|
107
|
+
columnHeaderComponent,
|
|
108
|
+
axisTopContent,
|
|
109
|
+
onClickTimescaleInterval,
|
|
110
|
+
onColumnMouseOver,
|
|
111
|
+
onColumnClick,
|
|
74
112
|
...scaleProps
|
|
75
113
|
}: CorrelationChartProps) {
|
|
76
114
|
const defaultScaleProps = {
|
|
@@ -115,9 +153,9 @@ export function CorrelationChart({
|
|
|
115
153
|
{ selectedUnit, onUnitSelected, units },
|
|
116
154
|
h(ChartArea, [
|
|
117
155
|
h(TimescaleColumn, {
|
|
118
|
-
key: "timescale",
|
|
119
156
|
scaleInfo,
|
|
120
157
|
unconformityLabels,
|
|
158
|
+
onClickInterval: onClickTimescaleInterval,
|
|
121
159
|
}),
|
|
122
160
|
h(MainChartArea, [
|
|
123
161
|
h(
|
|
@@ -126,7 +164,7 @@ export function CorrelationChart({
|
|
|
126
164
|
className,
|
|
127
165
|
innerWidth: mainWidth,
|
|
128
166
|
height: scaleInfo.totalHeight,
|
|
129
|
-
paddingH:
|
|
167
|
+
paddingH: chartPaddingH,
|
|
130
168
|
},
|
|
131
169
|
packages.map((pkg, i) => {
|
|
132
170
|
const { offset, domain, pixelScale, scale, key } =
|
|
@@ -141,6 +179,7 @@ export function CorrelationChart({
|
|
|
141
179
|
pixelScale,
|
|
142
180
|
scale,
|
|
143
181
|
unitComponent,
|
|
182
|
+
onColumnMouseOver,
|
|
144
183
|
});
|
|
145
184
|
}),
|
|
146
185
|
),
|
|
@@ -150,6 +189,17 @@ export function CorrelationChart({
|
|
|
150
189
|
// Navigation only works within a column for now...
|
|
151
190
|
h(UnitKeyboardNavigation, { columnData: data }),
|
|
152
191
|
]),
|
|
192
|
+
// Rendered last so the sticky header paints above the unit boxes
|
|
193
|
+
// (positioned siblings paint in document order at the same z-index)
|
|
194
|
+
h(ColumnHeaderRow, {
|
|
195
|
+
data,
|
|
196
|
+
columnWidth,
|
|
197
|
+
columnSpacing,
|
|
198
|
+
columnHeaderComponent,
|
|
199
|
+
axisTopContent,
|
|
200
|
+
onColumnMouseOver,
|
|
201
|
+
onColumnClick,
|
|
202
|
+
}),
|
|
153
203
|
]),
|
|
154
204
|
),
|
|
155
205
|
);
|
|
@@ -164,6 +214,7 @@ function Package({
|
|
|
164
214
|
domain,
|
|
165
215
|
pixelScale,
|
|
166
216
|
scale,
|
|
217
|
+
onColumnMouseOver,
|
|
167
218
|
}) {
|
|
168
219
|
return h("g.package", { transform: `translate(0 ${offset})` }, [
|
|
169
220
|
// Disable the SVG overlay for now
|
|
@@ -172,6 +223,7 @@ function Package({
|
|
|
172
223
|
columnData.map((data, i) => {
|
|
173
224
|
return h(Column, {
|
|
174
225
|
units: data.units,
|
|
226
|
+
columnID: data.columnID,
|
|
175
227
|
unitComponent,
|
|
176
228
|
width: columnWidth,
|
|
177
229
|
key: i,
|
|
@@ -179,6 +231,7 @@ function Package({
|
|
|
179
231
|
pixelScale,
|
|
180
232
|
scale,
|
|
181
233
|
offsetLeft: i * (columnWidth + columnSpacing),
|
|
234
|
+
onColumnMouseOver,
|
|
182
235
|
});
|
|
183
236
|
}),
|
|
184
237
|
]),
|
|
@@ -187,6 +240,7 @@ function Package({
|
|
|
187
240
|
|
|
188
241
|
interface ColumnProps {
|
|
189
242
|
units: ExtUnit[];
|
|
243
|
+
columnID?: number;
|
|
190
244
|
unitComponent?: React.FunctionComponent<any>;
|
|
191
245
|
unitComponentProps?: any;
|
|
192
246
|
showLabels?: boolean;
|
|
@@ -198,17 +252,20 @@ interface ColumnProps {
|
|
|
198
252
|
domain: [number, number];
|
|
199
253
|
pixelScale: number;
|
|
200
254
|
scale?: ScaleContinuousNumeric<number, number>;
|
|
255
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
201
256
|
}
|
|
202
257
|
|
|
203
258
|
function Column(props: ColumnProps) {
|
|
204
259
|
const {
|
|
205
260
|
units,
|
|
261
|
+
columnID,
|
|
206
262
|
width = 150,
|
|
207
263
|
offsetLeft,
|
|
208
264
|
domain,
|
|
209
265
|
pixelScale,
|
|
210
266
|
scale,
|
|
211
267
|
unitComponent = ColoredUnitComponent,
|
|
268
|
+
onColumnMouseOver,
|
|
212
269
|
} = props;
|
|
213
270
|
|
|
214
271
|
const columnWidth = width;
|
|
@@ -217,10 +274,19 @@ function Column(props: ColumnProps) {
|
|
|
217
274
|
return null;
|
|
218
275
|
}
|
|
219
276
|
|
|
277
|
+
const hoverHandlers =
|
|
278
|
+
onColumnMouseOver != null && columnID != null
|
|
279
|
+
? {
|
|
280
|
+
onMouseEnter: () => onColumnMouseOver(columnID),
|
|
281
|
+
onMouseLeave: () => onColumnMouseOver(null),
|
|
282
|
+
}
|
|
283
|
+
: {};
|
|
284
|
+
|
|
220
285
|
return h(
|
|
221
286
|
"g.section",
|
|
222
287
|
{
|
|
223
288
|
transform: `translate(${offsetLeft} 0)`,
|
|
289
|
+
...hoverHandlers,
|
|
224
290
|
},
|
|
225
291
|
h(
|
|
226
292
|
ColumnProvider,
|
|
@@ -235,7 +301,6 @@ function Column(props: ColumnProps) {
|
|
|
235
301
|
h(UnitBoxes, {
|
|
236
302
|
unitComponent,
|
|
237
303
|
unitComponentProps: {
|
|
238
|
-
nColumns: 2,
|
|
239
304
|
width: columnWidth,
|
|
240
305
|
showLabel: true,
|
|
241
306
|
},
|
|
@@ -322,16 +387,122 @@ function ChartArea({ children }) {
|
|
|
322
387
|
);
|
|
323
388
|
}
|
|
324
389
|
|
|
390
|
+
interface ColumnHeaderRowProps {
|
|
391
|
+
data: ColumnData[];
|
|
392
|
+
columnWidth: number;
|
|
393
|
+
columnSpacing: number;
|
|
394
|
+
columnHeaderComponent?: React.ComponentType<ColumnHeaderProps>;
|
|
395
|
+
axisTopContent?: React.ReactNode;
|
|
396
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
397
|
+
onColumnClick?: (columnID: number) => void;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function ColumnHeaderRow({
|
|
401
|
+
data,
|
|
402
|
+
columnWidth,
|
|
403
|
+
columnSpacing,
|
|
404
|
+
columnHeaderComponent,
|
|
405
|
+
axisTopContent,
|
|
406
|
+
onColumnMouseOver,
|
|
407
|
+
onColumnClick,
|
|
408
|
+
}: ColumnHeaderRowProps) {
|
|
409
|
+
/** The top grid row: arbitrary content above the timescale axis (top-left)
|
|
410
|
+
* and, when a header component is provided, per-column headers aligned with
|
|
411
|
+
* the columns. Collapses to nothing when neither is present. */
|
|
412
|
+
const Component = columnHeaderComponent;
|
|
413
|
+
if (Component == null && axisTopContent == null) {
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return h([
|
|
418
|
+
h("div.column-header-spacer", axisTopContent),
|
|
419
|
+
h.if(Component != null)(
|
|
420
|
+
"div.column-header-row",
|
|
421
|
+
{
|
|
422
|
+
style: {
|
|
423
|
+
paddingLeft: chartPaddingH,
|
|
424
|
+
paddingRight: chartPaddingH,
|
|
425
|
+
gap: columnSpacing,
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
data.map((column, i) => {
|
|
429
|
+
return h(ColumnHeaderCell, {
|
|
430
|
+
key: column.columnID ?? i,
|
|
431
|
+
column,
|
|
432
|
+
columnIndex: i,
|
|
433
|
+
columnWidth,
|
|
434
|
+
Component: Component!,
|
|
435
|
+
onColumnMouseOver,
|
|
436
|
+
onColumnClick,
|
|
437
|
+
});
|
|
438
|
+
}),
|
|
439
|
+
),
|
|
440
|
+
]);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function ColumnHeaderCell({
|
|
444
|
+
column,
|
|
445
|
+
columnIndex,
|
|
446
|
+
columnWidth,
|
|
447
|
+
Component,
|
|
448
|
+
onColumnMouseOver,
|
|
449
|
+
onColumnClick,
|
|
450
|
+
}: {
|
|
451
|
+
column: ColumnData;
|
|
452
|
+
columnIndex: number;
|
|
453
|
+
columnWidth: number;
|
|
454
|
+
Component: React.ComponentType<ColumnHeaderProps>;
|
|
455
|
+
onColumnMouseOver?: (columnID: number | null) => void;
|
|
456
|
+
onColumnClick?: (columnID: number) => void;
|
|
457
|
+
}) {
|
|
458
|
+
/** Resolves the column name and wires column-level hover/click so the header
|
|
459
|
+
* component itself can stay a pure function of props. */
|
|
460
|
+
const columnID = column.columnID;
|
|
461
|
+
const info = useMacrostratColumnInfo(columnID);
|
|
462
|
+
|
|
463
|
+
return h(
|
|
464
|
+
"div.column-header-cell",
|
|
465
|
+
{
|
|
466
|
+
style: {
|
|
467
|
+
width: columnWidth,
|
|
468
|
+
minWidth: columnWidth,
|
|
469
|
+
maxWidth: columnWidth,
|
|
470
|
+
},
|
|
471
|
+
onMouseEnter: () => onColumnMouseOver?.(columnID),
|
|
472
|
+
onMouseLeave: () => onColumnMouseOver?.(null),
|
|
473
|
+
onClick: onColumnClick
|
|
474
|
+
? (e: React.MouseEvent) => {
|
|
475
|
+
e.stopPropagation();
|
|
476
|
+
onColumnClick(columnID);
|
|
477
|
+
}
|
|
478
|
+
: undefined,
|
|
479
|
+
},
|
|
480
|
+
h(Component, {
|
|
481
|
+
column,
|
|
482
|
+
columnID,
|
|
483
|
+
columnIndex,
|
|
484
|
+
units: column.units as ExtUnit[],
|
|
485
|
+
width: columnWidth,
|
|
486
|
+
columnName: info?.col_name ?? null,
|
|
487
|
+
}),
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
325
491
|
interface TimescaleColumnProps {
|
|
326
492
|
scaleInfo: CompositeStratigraphicScaleInfo;
|
|
327
493
|
showLabels?: boolean;
|
|
328
494
|
unconformityLabels?: boolean;
|
|
495
|
+
onClickInterval?: TimescaleClickHandler;
|
|
329
496
|
}
|
|
330
497
|
|
|
331
498
|
function TimescaleColumn(props: TimescaleColumnProps) {
|
|
332
|
-
const { scaleInfo, unconformityLabels = true } = props;
|
|
499
|
+
const { scaleInfo, unconformityLabels = true, onClickInterval } = props;
|
|
333
500
|
return h("div.column-container.age-axis-container", [
|
|
334
501
|
h(CompositeAgeAxisCore, { ...scaleInfo }),
|
|
335
|
-
h(CompositeTimescaleCore, {
|
|
502
|
+
h(CompositeTimescaleCore, {
|
|
503
|
+
...scaleInfo,
|
|
504
|
+
unconformityLabels,
|
|
505
|
+
onClickInterval,
|
|
506
|
+
}),
|
|
336
507
|
]);
|
|
337
508
|
}
|
|
@@ -73,6 +73,17 @@ export function MacrostratColumnStateProvider<T extends BaseUnit>({
|
|
|
73
73
|
return selectedUnit != null || onUnitSelected != null;
|
|
74
74
|
}, []);
|
|
75
75
|
|
|
76
|
+
// Check if we're already in a columnStateProvider, and render a no-op if so
|
|
77
|
+
const existingUnits = scope.useAtomValueIfExists(columnUnitsAtom);
|
|
78
|
+
if (existingUnits != null) {
|
|
79
|
+
if (allowUnitSelection || onUnitSelected || selectedUnit != null) {
|
|
80
|
+
console.warn(
|
|
81
|
+
"MacrostratColumnStateProvider: unit selection props are ignored because a provider already exists in the tree",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
return children;
|
|
85
|
+
}
|
|
86
|
+
|
|
76
87
|
const atomMap: AtomMap = [
|
|
77
88
|
[columnUnitsAtom, units],
|
|
78
89
|
[allowUnitSelectionAtom, _allowSelection],
|
|
@@ -89,7 +100,6 @@ export function MacrostratColumnStateProvider<T extends BaseUnit>({
|
|
|
89
100
|
{
|
|
90
101
|
atoms: atomMap,
|
|
91
102
|
keepUpdated: true,
|
|
92
|
-
inherit: true,
|
|
93
103
|
},
|
|
94
104
|
[selectionHandlers, children],
|
|
95
105
|
);
|
|
@@ -156,12 +156,23 @@ if (props.onClickedColumn) {
|
|
|
156
156
|
|
|
157
157
|
export function useUnitSelectionTarget(
|
|
158
158
|
unit: IUnit,
|
|
159
|
-
): [React.RefObject<HTMLElement>, boolean, (evt: Event) => void] {
|
|
159
|
+
): [React.RefObject<HTMLElement>, boolean, (evt: Event) => void, boolean] {
|
|
160
160
|
const ref = useRef<HTMLElement>(null);
|
|
161
161
|
const [selectedUnit, selectUnit] = useUnitSelection();
|
|
162
162
|
const selected = selectedUnit?.unit_id == unit.unit_id;
|
|
163
163
|
const selectedUnitElement = scope.useAtomValue(selectedUnitElementAtom);
|
|
164
164
|
|
|
165
|
+
// "Linked" units share a stratigraphic name with the selected unit (e.g.
|
|
166
|
+
// the same formation correlated across columns), but are not the selection
|
|
167
|
+
// itself. They receive a subtler highlight.
|
|
168
|
+
const selectedStratName = (selectedUnit as any)?.strat_name_id;
|
|
169
|
+
const unitStratName = (unit as any)?.strat_name_id;
|
|
170
|
+
const linked =
|
|
171
|
+
!selected &&
|
|
172
|
+
selectedStratName != null &&
|
|
173
|
+
unitStratName != null &&
|
|
174
|
+
unitStratName === selectedStratName;
|
|
175
|
+
|
|
165
176
|
const onClick = useCallback(
|
|
166
177
|
(evt: Event) => {
|
|
167
178
|
selectUnit?.(unit, ref.current);
|
|
@@ -185,7 +196,7 @@ export function useUnitSelectionTarget(
|
|
|
185
196
|
});
|
|
186
197
|
}, [selectedUnitElement]);
|
|
187
198
|
|
|
188
|
-
return [ref, selected, onClick];
|
|
199
|
+
return [ref, selected, onClick, linked];
|
|
189
200
|
}
|
|
190
201
|
|
|
191
202
|
export function UnitKeyboardNavigation({
|
|
@@ -15,6 +15,7 @@ function ColumnCorrelationMapExt(
|
|
|
15
15
|
) {
|
|
16
16
|
const {
|
|
17
17
|
focusedLine,
|
|
18
|
+
manualColumns,
|
|
18
19
|
columns,
|
|
19
20
|
onSelectColumns,
|
|
20
21
|
projectID,
|
|
@@ -26,6 +27,7 @@ function ColumnCorrelationMapExt(
|
|
|
26
27
|
ColumnCorrelationProvider,
|
|
27
28
|
{
|
|
28
29
|
focusedLine,
|
|
30
|
+
manualColumns,
|
|
29
31
|
columns,
|
|
30
32
|
onSelectColumns,
|
|
31
33
|
projectID,
|
|
@@ -63,6 +65,16 @@ export const withPreloadedCrossSection = {
|
|
|
63
65
|
},
|
|
64
66
|
};
|
|
65
67
|
|
|
68
|
+
// Manual selection: click columns to add/remove them (no line of section).
|
|
69
|
+
// Hovering highlights a column; the dashed line shows the selection order.
|
|
70
|
+
export const manualColumnSelection = {
|
|
71
|
+
args: {
|
|
72
|
+
style: { width: "800px", height: "600px" },
|
|
73
|
+
padding: 100,
|
|
74
|
+
manualColumns: [432, 490],
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
|
|
66
78
|
export const nonStandardProject = {
|
|
67
79
|
args: {
|
|
68
80
|
style: { width: "800px", height: "600px" },
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
import { LngLatBounds, Style } from "mapbox-gl";
|
|
8
8
|
import h from "@macrostrat/hyper";
|
|
9
9
|
import { Feature, FeatureCollection } from "geojson";
|
|
10
|
-
import { ReactNode, useMemo } from "react";
|
|
10
|
+
import { ReactNode, useMemo, useRef } from "react";
|
|
11
11
|
import { setGeoJSON, buildGeoJSONSource } from "@macrostrat/mapbox-utils";
|
|
12
12
|
|
|
13
13
|
import { useCorrelationMapStore } from "./state";
|
|
@@ -36,12 +36,48 @@ export function ColumnCorrelationMap(props: CorrelationMapProps) {
|
|
|
36
36
|
h(ColumnsLayer, { color: columnColor }),
|
|
37
37
|
h(SelectedColumnsLayer),
|
|
38
38
|
h(MapClickHandler),
|
|
39
|
+
h(ColumnInteractionHandler),
|
|
40
|
+
h(HoveredColumnHighlight),
|
|
41
|
+
h(ColumnZoomer, { padding }),
|
|
39
42
|
h(SectionLine, { padding }),
|
|
40
43
|
children,
|
|
41
44
|
],
|
|
42
45
|
);
|
|
43
46
|
}
|
|
44
47
|
|
|
48
|
+
function ColumnZoomer({ padding }: { padding: number }) {
|
|
49
|
+
/** Frame a single column when `zoomColumn` is set (e.g. header click). */
|
|
50
|
+
const zoomColumn = useCorrelationMapStore((state) => state.zoomColumn);
|
|
51
|
+
const zoomNonce = useCorrelationMapStore((state) => state.zoomNonce);
|
|
52
|
+
const columns = useCorrelationMapStore((state) => state.columns);
|
|
53
|
+
|
|
54
|
+
const bounds = useMemo(() => {
|
|
55
|
+
if (zoomColumn == null || columns == null) return null;
|
|
56
|
+
const col = columns.find((c) => c.properties?.col_id === zoomColumn);
|
|
57
|
+
if (col?.geometry == null) return null;
|
|
58
|
+
const b = new LngLatBounds();
|
|
59
|
+
forEachCoordinate(col.geometry, (coord) => b.extend(coord as [number, number]));
|
|
60
|
+
return b.isEmpty() ? null : b;
|
|
61
|
+
// zoomNonce forces a fresh bounds object so repeat clicks re-frame
|
|
62
|
+
}, [zoomColumn, zoomNonce, columns]);
|
|
63
|
+
|
|
64
|
+
useMapEaseTo({ bounds, padding: padding * 2 });
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function forEachCoordinate(geometry: any, fn: (coord: number[]) => void) {
|
|
69
|
+
const walk = (coords: any) => {
|
|
70
|
+
if (typeof coords[0] === "number") {
|
|
71
|
+
fn(coords);
|
|
72
|
+
} else {
|
|
73
|
+
for (const c of coords) walk(c);
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
if (geometry?.coordinates != null) walk(geometry.coordinates);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const columnLayers = ["columns-fill", "columns-points"];
|
|
80
|
+
|
|
45
81
|
function MapClickHandler() {
|
|
46
82
|
const onClickMap = useCorrelationMapStore((state) => state.onClickMap);
|
|
47
83
|
|
|
@@ -55,6 +91,102 @@ function MapClickHandler() {
|
|
|
55
91
|
return null;
|
|
56
92
|
}
|
|
57
93
|
|
|
94
|
+
function ColumnInteractionHandler() {
|
|
95
|
+
/** Per-column click (manual selection) and hover highlighting. */
|
|
96
|
+
const toggleColumn = useCorrelationMapStore((state) => state.toggleColumn);
|
|
97
|
+
const setHoveredColumn = useCorrelationMapStore(
|
|
98
|
+
(state) => state.setHoveredColumn,
|
|
99
|
+
);
|
|
100
|
+
const isManual = useCorrelationMapStore(
|
|
101
|
+
(state) => state.selectionMode === "manual",
|
|
102
|
+
);
|
|
103
|
+
const hoveredRef = useRef<number | string | null>(null);
|
|
104
|
+
|
|
105
|
+
// Click a column to toggle it in/out of the selection (manual mode only)
|
|
106
|
+
useMapStyleOperator(
|
|
107
|
+
(map) => {
|
|
108
|
+
if (!isManual) return;
|
|
109
|
+
const onClick = (e) => {
|
|
110
|
+
const colID = e.features?.[0]?.properties?.col_id;
|
|
111
|
+
if (colID != null) toggleColumn(colID);
|
|
112
|
+
};
|
|
113
|
+
map.on("click", columnLayers, onClick);
|
|
114
|
+
return () => map.off("click", columnLayers, onClick);
|
|
115
|
+
},
|
|
116
|
+
[isManual, toggleColumn],
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Hover highlighting (via the columns layer's `hover` feature-state)
|
|
120
|
+
useMapStyleOperator(
|
|
121
|
+
(map) => {
|
|
122
|
+
const clearHover = () => {
|
|
123
|
+
if (hoveredRef.current != null) {
|
|
124
|
+
map.setFeatureState(
|
|
125
|
+
{ source: "columns", id: hoveredRef.current },
|
|
126
|
+
{ hover: false },
|
|
127
|
+
);
|
|
128
|
+
hoveredRef.current = null;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
const onMove = (e) => {
|
|
132
|
+
const f = e.features?.[0];
|
|
133
|
+
if (f == null) return;
|
|
134
|
+
if (hoveredRef.current !== f.id) {
|
|
135
|
+
clearHover();
|
|
136
|
+
map.setFeatureState({ source: "columns", id: f.id }, { hover: true });
|
|
137
|
+
hoveredRef.current = f.id;
|
|
138
|
+
}
|
|
139
|
+
setHoveredColumn(f.properties?.col_id ?? null);
|
|
140
|
+
map.getCanvas().style.cursor = "pointer";
|
|
141
|
+
};
|
|
142
|
+
const onLeave = () => {
|
|
143
|
+
clearHover();
|
|
144
|
+
setHoveredColumn(null);
|
|
145
|
+
map.getCanvas().style.cursor = "";
|
|
146
|
+
};
|
|
147
|
+
map.on("mousemove", columnLayers, onMove);
|
|
148
|
+
map.on("mouseleave", columnLayers, onLeave);
|
|
149
|
+
return () => {
|
|
150
|
+
map.off("mousemove", columnLayers, onMove);
|
|
151
|
+
map.off("mouseleave", columnLayers, onLeave);
|
|
152
|
+
clearHover();
|
|
153
|
+
};
|
|
154
|
+
},
|
|
155
|
+
[setHoveredColumn],
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function HoveredColumnHighlight() {
|
|
162
|
+
/** Reflects the store's `hoveredColumn` on the map (e.g. when a column is
|
|
163
|
+
* hovered elsewhere in the UI) using the `highlighted` feature-state. */
|
|
164
|
+
const hoveredColumn = useCorrelationMapStore((state) => state.hoveredColumn);
|
|
165
|
+
const columns = useCorrelationMapStore((state) => state.columns);
|
|
166
|
+
const appliedRef = useRef<number | string | null>(null);
|
|
167
|
+
|
|
168
|
+
useMapStyleOperator(
|
|
169
|
+
(map) => {
|
|
170
|
+
if (appliedRef.current != null) {
|
|
171
|
+
map.setFeatureState(
|
|
172
|
+
{ source: "columns", id: appliedRef.current },
|
|
173
|
+
{ highlighted: false },
|
|
174
|
+
);
|
|
175
|
+
appliedRef.current = null;
|
|
176
|
+
}
|
|
177
|
+
if (hoveredColumn == null || columns == null) return;
|
|
178
|
+
const col = columns.find((c) => c.properties?.col_id === hoveredColumn);
|
|
179
|
+
const id = (col as any)?.id ?? col?.properties?.col_id;
|
|
180
|
+
if (id == null) return;
|
|
181
|
+
map.setFeatureState({ source: "columns", id }, { highlighted: true });
|
|
182
|
+
appliedRef.current = id;
|
|
183
|
+
},
|
|
184
|
+
[hoveredColumn, columns],
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
58
190
|
function SelectedColumnsLayer() {
|
|
59
191
|
useOverlayStyle(() => selectedColumnsStyle, []);
|
|
60
192
|
|
|
@@ -148,11 +280,17 @@ const lineOfSectionStyle: Style = {
|
|
|
148
280
|
function SectionLine({ padding }: { padding: number }) {
|
|
149
281
|
useOverlayStyle(() => lineOfSectionStyle, []);
|
|
150
282
|
const focusedLine = useCorrelationMapStore((state) => state.focusedLine);
|
|
283
|
+
const focusedColumns = useCorrelationMapStore(
|
|
284
|
+
(state) => state.focusedColumns,
|
|
285
|
+
);
|
|
151
286
|
|
|
152
|
-
// Setup focused line
|
|
287
|
+
// Setup focused line (only drawn in line-of-section mode)
|
|
153
288
|
useMapStyleOperator(
|
|
154
289
|
(map) => {
|
|
155
290
|
if (focusedLine == null) {
|
|
291
|
+
// Clear any previously-drawn line (e.g. after switching to manual mode)
|
|
292
|
+
setGeoJSON(map, "crossSectionLine", emptyFeatureCollection);
|
|
293
|
+
setGeoJSON(map, "crossSectionEndpoints", emptyFeatureCollection);
|
|
156
294
|
return;
|
|
157
295
|
}
|
|
158
296
|
const data: FeatureCollection = {
|
|
@@ -180,17 +318,25 @@ function SectionLine({ padding }: { padding: number }) {
|
|
|
180
318
|
);
|
|
181
319
|
|
|
182
320
|
const bounds = useMemo(() => {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
321
|
+
// In line mode, frame the line; in manual mode, frame the selected columns
|
|
322
|
+
const coords: number[][] =
|
|
323
|
+
focusedLine != null && focusedLine.coordinates.length >= 2
|
|
324
|
+
? focusedLine.coordinates
|
|
325
|
+
: focusedColumns.map((c) => c.properties.centroid.geometry.coordinates);
|
|
326
|
+
if (coords.length < 2) return null;
|
|
327
|
+
const bounds = new LngLatBounds();
|
|
328
|
+
for (const coord of coords) {
|
|
329
|
+
bounds.extend(coord as [number, number]);
|
|
189
330
|
}
|
|
190
331
|
return bounds;
|
|
191
|
-
}, [focusedLine]);
|
|
332
|
+
}, [focusedLine, focusedColumns]);
|
|
192
333
|
|
|
193
334
|
useMapEaseTo({ bounds, padding, trackResize: true });
|
|
194
335
|
|
|
195
336
|
return null;
|
|
196
337
|
}
|
|
338
|
+
|
|
339
|
+
const emptyFeatureCollection: FeatureCollection = {
|
|
340
|
+
type: "FeatureCollection",
|
|
341
|
+
features: [],
|
|
342
|
+
};
|