Skip to content

Commit 328fa86

Browse files
committed
* 'main' of https://github.com/UW-Macrostrat/web-components: Update column selection and age axis Split out mouse event handlers Updated column mouse over
2 parents 99a7ea9 + 1d1eb63 commit 328fa86

File tree

9 files changed

+276
-25
lines changed

9 files changed

+276
-25
lines changed

packages/column-views/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format
44
is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this
55
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
- Add mouseover handlers to allow age cursor to be reported
10+
- Add an `AgeCursor` component
11+
712
## [2.0.1] - 2025-05-08
813

914
Solve a problem with strict mode

packages/column-views/src/age-axis.module.sass

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,23 @@
2525
max-height: 100vh
2626
.age-axis-unit
2727
color: var(--text-subtle-color)
28+
29+
.age-cursor
30+
position: absolute
31+
left: 30px
32+
right: 0
33+
pointer-events: none
34+
.line
35+
border-bottom: 2px solid var(--column-axis-cursor-color, var(--column-cursor-color, var(--column-stroke-color)))
36+
transform: translateY(-1px)
37+
38+
.label
39+
writing-mode: vertical-lr
40+
padding: 4px 2px
41+
transform: rotate(180deg) translate(16px, 50%)
42+
background-color: var(--column-background-color)
43+
text-align: center
44+
45+
46+
47+

packages/column-views/src/age-axis.ts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import hyper from "@macrostrat/hyper";
22
import {
33
SVG,
4-
ColumnSVG,
54
ColumnAxis,
65
ColumnContext,
76
ColumnAxisType,
87
AgeAxis,
98
} from "@macrostrat/column-components";
109
import { useContext } from "react";
1110
import styles from "./age-axis.module.sass";
12-
import { useMacrostratColumnData } from "./data-provider";
11+
import { useCompositeScale, useMacrostratColumnData } from "./data-provider";
1312
import { Parenthetical } from "@macrostrat/data-components";
1413
import { PackageScaleLayoutData } from "./prepare-units/composite-scale";
14+
import { AgeLabel } from "./unit-details";
1515

1616
const h = hyper.styled(styles);
1717

@@ -103,3 +103,25 @@ export function CompositeAgeAxisCore(props: CompositeStratigraphicScaleInfo) {
103103
),
104104
]);
105105
}
106+
107+
export function AgeCursor({ age }) {
108+
/** A cursor that shows the age at a specific point on the age axis. */
109+
const scale = useCompositeScale();
110+
const heightPx = scale(age);
111+
112+
console.log(age, heightPx);
113+
114+
if (age == null || heightPx == null) {
115+
return null;
116+
}
117+
118+
return h(
119+
"div.age-cursor",
120+
{
121+
style: {
122+
top: heightPx,
123+
},
124+
},
125+
[h("div.line"), h(AgeLabel, { age, className: "label" })]
126+
);
127+
}

packages/column-views/src/column.ts

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ColumnAxisType } from "@macrostrat/column-components";
22
import { hyperStyled } from "@macrostrat/hyper";
33
import { useDarkMode } from "@macrostrat/ui-components";
44
import classNames from "classnames";
5-
import { RefObject, useRef, HTMLAttributes } from "react";
5+
import { RefObject, useRef, HTMLAttributes, useCallback } from "react";
66
import styles from "./column.module.sass";
77
import {
88
UnitSelectionProvider,
@@ -15,6 +15,7 @@ import { ColumnHeightScaleOptions } from "./prepare-units/composite-scale";
1515
import { UnitSelectionPopover } from "./unit-details";
1616
import {
1717
MacrostratColumnDataProvider,
18+
useCompositeScale,
1819
useMacrostratColumnData,
1920
} from "./data-provider";
2021
import {
@@ -38,6 +39,11 @@ interface BaseColumnProps extends SectionSharedProps {
3839
// Timescale properties
3940
showTimescale?: boolean;
4041
timescaleLevels?: number | [number, number];
42+
onMouseOver?: (
43+
unit: UnitLong | null,
44+
height: number | null,
45+
evt: MouseEvent
46+
) => void;
4147
}
4248

4349
export interface ColumnProps extends BaseColumnProps, ColumnHeightScaleOptions {
@@ -134,12 +140,11 @@ function ColumnInner(props: ColumnInnerProps) {
134140
showTimescale,
135141
timescaleLevels,
136142
maxInternalColumns,
143+
onMouseOver,
137144
} = props;
138145

139146
const { axisType } = useMacrostratColumnData();
140147

141-
const dispatch = useUnitSelectionDispatch();
142-
143148
let width = _width;
144149
let columnWidth = _columnWidth;
145150
if (columnWidth > width) {
@@ -159,9 +164,7 @@ function ColumnInner(props: ColumnInnerProps) {
159164
return h(
160165
ColumnContainer,
161166
{
162-
onClick(evt) {
163-
dispatch?.(null, null, evt as any);
164-
},
167+
...useMouseEventHandlers(onMouseOver),
165168
className,
166169
},
167170
h("div.column", { ref: columnRef }, [
@@ -182,6 +185,55 @@ function ColumnInner(props: ColumnInnerProps) {
182185
);
183186
}
184187

188+
type ColumnMouseOverHandler = (
189+
unit: UnitLong | null,
190+
height: number | null,
191+
evt: MouseEvent
192+
) => void;
193+
194+
function useMouseEventHandlers(
195+
_onMouseOver: ColumnMouseOverHandler | null = null
196+
) {
197+
/** Click event handler */
198+
199+
// Click handler for unit selection
200+
const dispatch = useUnitSelectionDispatch();
201+
const onClick = useCallback(
202+
(evt) => {
203+
dispatch?.(null, null, evt as any);
204+
},
205+
[dispatch]
206+
);
207+
208+
/** Hover event handlers */
209+
const scale = useCompositeScale();
210+
211+
const onMouseOver = useCallback(
212+
(evt) => {
213+
const height = scale.invert(
214+
evt.clientY - evt.currentTarget.getBoundingClientRect().top
215+
);
216+
_onMouseOver?.(null, height, evt);
217+
},
218+
[scale, _onMouseOver]
219+
);
220+
221+
const onMouseOut = useCallback(() => {
222+
_onMouseOver?.(null, null, null);
223+
}, [_onMouseOver]);
224+
225+
if (_onMouseOver == null) {
226+
return {
227+
onClick,
228+
onMouseOver: undefined,
229+
onMouseMove: undefined,
230+
onMouseOut: undefined,
231+
};
232+
}
233+
234+
return { onMouseOver, onMouseMove: onMouseOver, onMouseOut, onClick };
235+
}
236+
185237
export interface ColumnContainerProps extends HTMLAttributes<HTMLDivElement> {
186238
className?: string;
187239
}

packages/column-views/src/prepare-units/composite-scale.ts

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { ColumnAxisType } from "@macrostrat/column-components";
33
import { ensureArray, getUnitHeightRange } from "./utils";
44
import { ScaleLinear, scaleLinear } from "d3-scale";
55
import { UnitLong } from "@macrostrat/api-types";
6+
import { CompositeColumnScale } from "@macrostrat/column-views";
67

78
export interface ColumnHeightScaleOptions {
89
/** A fixed pixel scale to use for the section (pixels per Myr) */
@@ -268,41 +269,79 @@ function findAverageUnitHeight(
268269
return unitHeights.reduce((a, b) => a + b, 0) / unitHeights.length;
269270
}
270271

272+
interface CompositeColumnScale {
273+
(age: number): number | null;
274+
copy(): CompositeColumnScale;
275+
domain(): [number, number];
276+
invert(pixelHeight: number): number | null;
277+
}
278+
271279
export function createCompositeScale(
272280
sections: PackageLayoutData[],
273281
interpolateUnconformities: boolean = false
274-
): (age: number) => number | null {
282+
): CompositeColumnScale {
275283
/** Create a scale that works across multiple packages */
276284
// Get surfaces at which scale breaks
277285
let scaleBreaks: [number, number][] = [];
278286
for (const section of sections) {
279-
const { pixelHeight, pixelScale, offset, domain } = section.scaleInfo;
287+
const { pixelHeight, offset, domain } = section.scaleInfo;
280288

281289
scaleBreaks.push([domain[1], offset]);
282290
scaleBreaks.push([domain[0], offset + pixelHeight]);
283291
}
284292
// Sort the scale breaks by age
285293
scaleBreaks.sort((a, b) => a[0] - b[0]);
286294

287-
return (age) => {
288-
/** Given an age, find the corresponding pixel position */
289-
// Iterate through the sections to find the correct one
290-
295+
const scale = (age) => {
291296
// Accumulate scale breaks and pixel height
292-
let pixelHeight = 0;
293-
let pixelScale = 0;
297+
let lastHeight = 0;
294298
let lastAge = null;
295299
for (const [age1, height] of scaleBreaks) {
296300
if (age <= age1) {
297-
if (lastAge != null) {
298-
pixelHeight += (age - lastAge) * pixelScale;
299-
return pixelHeight;
300-
}
301-
lastAge = age;
302-
pixelHeight = height;
301+
let pixelScale = (height - lastHeight) / (age1 - lastAge);
302+
return lastHeight + (age - lastAge) * pixelScale;
303303
}
304+
lastAge = age1;
305+
lastHeight = height;
304306
}
305307
};
308+
309+
scale.copy = () => {
310+
return createCompositeScale(sections, interpolateUnconformities);
311+
};
312+
313+
scale.domain = () => {
314+
/** Return the domain of the scale */
315+
const firstSection = sections[0].scaleInfo.domain;
316+
const lastSection = sections[sections.length - 1].scaleInfo.domain;
317+
return [firstSection[0], lastSection[1]];
318+
};
319+
320+
scale.invert = (pixelHeight) => {
321+
/** Invert the scale to get the age at a given pixel height */
322+
// Iterate through the sections to find the correct one
323+
let lastAge = null;
324+
for (const section of sections) {
325+
const {
326+
pixelHeight: sectionHeight,
327+
pixelScale,
328+
offset,
329+
domain,
330+
} = section.scaleInfo;
331+
if (
332+
pixelHeight >= offset &&
333+
pixelHeight <= offset + sectionHeight &&
334+
pixelScale > 0
335+
) {
336+
const age = domain[1] + (pixelHeight - offset) / pixelScale;
337+
return age;
338+
}
339+
lastAge = domain[1];
340+
}
341+
return null;
342+
};
343+
344+
return scale as CompositeColumnScale;
306345
}
307346

308347
/** Collapse sections separated by unconformities that are smaller than a given pixel height. */

packages/column-views/src/unit-details/panel.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,48 @@ function getAgeRange(_unit) {
346346
return [b_age, t_age, unit];
347347
}
348348

349+
function getAge(value) {
350+
/** Get the age value in Ma, ka, or Ga as appropriate */
351+
let unit = "Ma";
352+
if (value < 0.8) {
353+
unit = "ka";
354+
value *= 1000;
355+
if (value < 5) {
356+
unit = "yr";
357+
value *= 1000;
358+
}
359+
} else if (value > 1000) {
360+
unit = "Ga";
361+
value /= 1000;
362+
}
363+
364+
return [value, unit];
365+
}
366+
367+
export function AgeLabel({
368+
age,
369+
maximumFractionDigits = 2,
370+
minimumFractionDigits = 0,
371+
className,
372+
}: {
373+
age: number;
374+
className?: string;
375+
maximumFractionDigits?: number;
376+
minimumFractionDigits?: number;
377+
}) {
378+
/** Component to display a single age value with unit conversion from
379+
* Ma to ka or Ga as appropriate.
380+
*/
381+
const [value, unit] = getAge(age);
382+
383+
const _value = value.toLocaleString("en-US", {
384+
maximumFractionDigits,
385+
minimumFractionDigits,
386+
});
387+
388+
return h(Value, { value: _value, unit, className });
389+
}
390+
349391
export function Duration({
350392
value,
351393
maximumFractionDigits = 2,

0 commit comments

Comments
 (0)