@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.
Files changed (138) hide show
  1. package/CHANGELOG.md +144 -0
  2. package/dist/animated-age-window.cjs +51 -0
  3. package/dist/animated-age-window.cjs.map +1 -0
  4. package/dist/animated-age-window.d.ts +51 -0
  5. package/dist/animated-age-window.js +51 -0
  6. package/dist/animated-age-window.js.map +1 -0
  7. package/dist/column-views.css +118 -89
  8. package/dist/column.cjs +12 -3
  9. package/dist/column.cjs.map +1 -1
  10. package/dist/column.d.ts +11 -0
  11. package/dist/column.js +12 -3
  12. package/dist/column.js.map +1 -1
  13. package/dist/column.module.sass.cjs +15 -15
  14. package/dist/column.module.sass.js +15 -15
  15. package/dist/correlation-chart/main.cjs +156 -42
  16. package/dist/correlation-chart/main.cjs.map +1 -1
  17. package/dist/correlation-chart/main.d.ts +40 -2
  18. package/dist/correlation-chart/main.js +138 -23
  19. package/dist/correlation-chart/main.js.map +1 -1
  20. package/dist/correlation-chart/main.module.sass.cjs +23 -20
  21. package/dist/correlation-chart/main.module.sass.cjs.map +1 -1
  22. package/dist/correlation-chart/main.module.sass.js +23 -20
  23. package/dist/correlation-chart/main.module.sass.js.map +1 -1
  24. package/dist/correlation-chart/prepare-data.cjs +0 -1
  25. package/dist/correlation-chart/prepare-data.cjs.map +1 -1
  26. package/dist/correlation-chart/prepare-data.js +0 -1
  27. package/dist/correlation-chart/prepare-data.js.map +1 -1
  28. package/dist/data-provider/store.cjs +17 -4
  29. package/dist/data-provider/store.cjs.map +1 -1
  30. package/dist/data-provider/store.d.ts +6 -1
  31. package/dist/data-provider/store.js +17 -4
  32. package/dist/data-provider/store.js.map +1 -1
  33. package/dist/data-provider/unit-selection.cjs +4 -1
  34. package/dist/data-provider/unit-selection.cjs.map +1 -1
  35. package/dist/data-provider/unit-selection.d.ts +1 -1
  36. package/dist/data-provider/unit-selection.js +4 -1
  37. package/dist/data-provider/unit-selection.js.map +1 -1
  38. package/dist/index.cjs +16 -4
  39. package/dist/index.cjs.map +1 -1
  40. package/dist/index.d.ts +1 -0
  41. package/dist/index.js +5 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/maps/column-correlation/core.cjs +127 -6
  44. package/dist/maps/column-correlation/core.cjs.map +1 -1
  45. package/dist/maps/column-correlation/core.js +128 -7
  46. package/dist/maps/column-correlation/core.js.map +1 -1
  47. package/dist/maps/column-correlation/state.cjs +63 -2
  48. package/dist/maps/column-correlation/state.cjs.map +1 -1
  49. package/dist/maps/column-correlation/state.d.ts +31 -1
  50. package/dist/maps/column-correlation/state.js +63 -2
  51. package/dist/maps/column-correlation/state.js.map +1 -1
  52. package/dist/maps/layers/columns.cjs +5 -1
  53. package/dist/maps/layers/columns.cjs.map +1 -1
  54. package/dist/maps/layers/columns.js +5 -1
  55. package/dist/maps/layers/columns.js.map +1 -1
  56. package/dist/prepare-units/composite-scale.cjs +94 -8
  57. package/dist/prepare-units/composite-scale.cjs.map +1 -1
  58. package/dist/prepare-units/composite-scale.d.ts +39 -0
  59. package/dist/prepare-units/composite-scale.js +96 -10
  60. package/dist/prepare-units/composite-scale.js.map +1 -1
  61. package/dist/prepare-units/helpers.cjs +0 -1
  62. package/dist/prepare-units/helpers.cjs.map +1 -1
  63. package/dist/prepare-units/helpers.js +0 -1
  64. package/dist/prepare-units/helpers.js.map +1 -1
  65. package/dist/prepare-units/index.cjs +33 -12
  66. package/dist/prepare-units/index.cjs.map +1 -1
  67. package/dist/prepare-units/index.js +34 -13
  68. package/dist/prepare-units/index.js.map +1 -1
  69. package/dist/prepare-units/types.cjs.map +1 -1
  70. package/dist/prepare-units/types.d.ts +13 -0
  71. package/dist/prepare-units/types.js.map +1 -1
  72. package/dist/section.cjs +10 -2
  73. package/dist/section.cjs.map +1 -1
  74. package/dist/section.d.ts +5 -0
  75. package/dist/section.js +10 -2
  76. package/dist/section.js.map +1 -1
  77. package/dist/unit-details/age-range.cjs +2 -1
  78. package/dist/unit-details/age-range.cjs.map +1 -1
  79. package/dist/unit-details/age-range.js +2 -1
  80. package/dist/unit-details/age-range.js.map +1 -1
  81. package/dist/unit-details/index.d.ts +1 -1
  82. package/dist/unit-details/panel.cjs +2 -3
  83. package/dist/unit-details/panel.cjs.map +1 -1
  84. package/dist/unit-details/panel.js +1 -2
  85. package/dist/unit-details/panel.js.map +1 -1
  86. package/dist/unit-details/panel.module.sass.cjs +18 -22
  87. package/dist/unit-details/panel.module.sass.cjs.map +1 -1
  88. package/dist/unit-details/panel.module.sass.js +18 -22
  89. package/dist/unit-details/panel.module.sass.js.map +1 -1
  90. package/dist/unit-details/strat-names.cjs +1 -2
  91. package/dist/unit-details/strat-names.cjs.map +1 -1
  92. package/dist/unit-details/strat-names.js +1 -2
  93. package/dist/unit-details/strat-names.js.map +1 -1
  94. package/dist/units/boxes.cjs +20 -36
  95. package/dist/units/boxes.cjs.map +1 -1
  96. package/dist/units/boxes.js +22 -38
  97. package/dist/units/boxes.js.map +1 -1
  98. package/dist/units/composite.cjs +12 -12
  99. package/dist/units/composite.cjs.map +1 -1
  100. package/dist/units/composite.js +12 -12
  101. package/dist/units/composite.js.map +1 -1
  102. package/package.json +9 -9
  103. package/src/animated-age-window.ts +120 -0
  104. package/src/column.module.sass +6 -0
  105. package/src/column.ts +24 -0
  106. package/src/correlation-chart/main.module.sass +46 -2
  107. package/src/correlation-chart/main.ts +210 -17
  108. package/src/correlation-chart/prepare-data.ts +0 -1
  109. package/src/data-provider/store.ts +21 -2
  110. package/src/data-provider/unit-selection.ts +13 -2
  111. package/src/index.ts +1 -0
  112. package/src/maps/column-correlation/column-correlation-map.stories.ts +12 -0
  113. package/src/maps/column-correlation/core.ts +155 -9
  114. package/src/maps/column-correlation/state.ts +110 -2
  115. package/src/maps/layers/columns.ts +7 -1
  116. package/src/prepare-units/composite-scale.ts +210 -10
  117. package/src/prepare-units/helpers.ts +0 -2
  118. package/src/prepare-units/index.ts +68 -14
  119. package/src/prepare-units/types.ts +13 -0
  120. package/src/section.ts +15 -1
  121. package/src/unit-details/age-range.ts +3 -1
  122. package/src/unit-details/index.ts +7 -1
  123. package/src/unit-details/panel.module.sass +0 -8
  124. package/src/unit-details/panel.ts +6 -6
  125. package/src/unit-details/strat-names.ts +1 -1
  126. package/src/units/boxes.ts +33 -53
  127. package/src/units/composite.ts +21 -12
  128. package/dist/correlation-chart/stories/utils.d.ts +0 -6
  129. package/dist/unit-details/identifiers.cjs +0 -56
  130. package/dist/unit-details/identifiers.cjs.map +0 -1
  131. package/dist/unit-details/identifiers.d.ts +0 -31
  132. package/dist/unit-details/identifiers.js +0 -54
  133. package/dist/unit-details/identifiers.js.map +0 -1
  134. package/src/correlation-chart/stories/correlation-chart.stories.ts +0 -228
  135. package/src/correlation-chart/stories/namibia-correlation.stories.ts +0 -151
  136. package/src/correlation-chart/stories/stories.module.sass +0 -18
  137. package/src/correlation-chart/stories/utils.ts +0 -24
  138. 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
+ }
@@ -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: flex
111
- flex-direction: row
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 hyper from "@macrostrat/hyper";
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 "../data-provider";
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
- const h = hyper.styled(styles);
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
- ...defaultScaleProps,
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: 4,
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, { ...scaleInfo, unconformityLabels }),
524
+ h(CompositeTimescaleCore, {
525
+ ...scaleInfo,
526
+ unconformityLabels,
527
+ onClickInterval,
528
+ }),
336
529
  ]);
337
530
  }
@@ -44,7 +44,6 @@ export function buildCorrelationChartData(
44
44
  columns: ColumnData[],
45
45
  settings: CorrelationChartSettings,
46
46
  ): CorrelationChartData {
47
- console.log(settings);
48
47
  const {
49
48
  mergeSections = MergeSectionsMode.OVERLAPPING,
50
49
  targetUnitHeight,