From cbd660373e1ca1f7bf45b7c06b57436c96eb872c Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 21 Nov 2025 04:21:28 -0600 Subject: [PATCH 01/46] Improve column status code loading --- .../column-views/src/data-provider/base.ts | 27 +++++-------------- .../stories/column-ui/standalone.ts | 10 +++++-- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/packages/column-views/src/data-provider/base.ts b/packages/column-views/src/data-provider/base.ts index 944c0c9e..c9c222af 100644 --- a/packages/column-views/src/data-provider/base.ts +++ b/packages/column-views/src/data-provider/base.ts @@ -121,35 +121,22 @@ function createColumnsSlice(set, get) { const { columnFootprints, baseURL, fetch } = get(); const key = projectID ?? -1; let _inProcess = inProcess; - if (projectID == null) { - // If no project is specified, in process columns cannot be included - _inProcess = false; - } + let footprints = columnFootprints.get(key); if (footprints == null || footprints.inProcess != _inProcess) { // Fetch the columns - const statusCode = inProcess ? "in process" : null; - let columns = await fetchAllColumns({ + const statusCode = ["active"]; + if (_inProcess) { + statusCode.push("in process"); + } + const columns = await fetchAllColumns({ projectID, - statusCode: null, + statusCode: statusCode.join(","), fetch, }); if (columns == null) { return; } - if (_inProcess) { - const inProcessColumns = await fetchAllColumns({ - projectID, - statusCode: "in process", - fetch, - }); - if (inProcessColumns == null) { - return; - } - // Combine active and in-process columns - columns = columns.concat(inProcessColumns); - } - footprints = { project_id: projectID, inProcess, diff --git a/packages/column-views/stories/column-ui/standalone.ts b/packages/column-views/stories/column-ui/standalone.ts index e71ae3cf..c8fa3012 100644 --- a/packages/column-views/stories/column-ui/standalone.ts +++ b/packages/column-views/stories/column-ui/standalone.ts @@ -6,7 +6,10 @@ import "@macrostrat/style-system"; import { ColumnProps } from "../../src"; function useColumnUnits(col_id, inProcess) { - const status_code = inProcess ? "in process" : undefined; + const status_codes = ["active"]; + if (inProcess) status_codes.push("in process"); + const status_code = status_codes.join(","); + // show_position is needed to properly deal with `section` column types. return useAPIResult( "https://dev.macrostrat.org/api/v2/units", @@ -16,7 +19,10 @@ function useColumnUnits(col_id, inProcess) { } function useColumnBasicInfo(col_id, inProcess = false) { - const status_code = inProcess ? "in process" : undefined; + const status_codes = ["active"]; + if (inProcess) status_codes.push("in process"); + const status_code = status_codes.join(","); + return useAPIResult( "https://dev.macrostrat.org/api/v2/columns", { col_id, status_code }, From 19bac10c09e0e249d7eb5a89b8f62ffae52b9bdc Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 21 Nov 2025 05:33:27 -0600 Subject: [PATCH 02/46] Started clipping units of eODP columns --- packages/column-views/src/column.ts | 6 +++ .../column-views/src/prepare-units/helpers.ts | 50 ++++++------------- .../column-views/src/prepare-units/index.ts | 10 +++- .../column-views/stories/sections.stories.ts | 20 ++++++++ packages/stratigraphy-utils/src/age-ranges.ts | 2 + 5 files changed, 51 insertions(+), 37 deletions(-) diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index f0762851..5fa93760 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -56,6 +56,8 @@ export interface ColumnProps extends BaseColumnProps, ColumnHeightScaleOptions { units: UnitLong[]; t_age?: number; b_age?: number; + t_pos?: number; + b_pos?: number; mergeSections?: MergeSectionsMode; showUnitPopover?: boolean; allowUnitSelection?: boolean; @@ -77,6 +79,8 @@ export function Column(props: ColumnProps) { axisType = ColumnAxisType.AGE, t_age, b_age, + t_pos, + b_pos, unconformityHeight = 30, targetUnitHeight = 20, pixelScale, @@ -93,6 +97,8 @@ export function Column(props: ColumnProps) { axisType, t_age, b_age, + t_pos, + b_pos, mergeSections, targetUnitHeight, unconformityHeight, diff --git a/packages/column-views/src/prepare-units/helpers.ts b/packages/column-views/src/prepare-units/helpers.ts index 9e3d56d9..010d3dfb 100644 --- a/packages/column-views/src/prepare-units/helpers.ts +++ b/packages/column-views/src/prepare-units/helpers.ts @@ -43,7 +43,10 @@ export function preprocessUnits( axisType: ColumnAxisType = ColumnAxisType.AGE, ): ExtUnit[] { /** Preprocess units to add overlapping units and columns. */ - const units = section.units; + let units = section.units; + + units = units.map(preprocessSectionUnit); + let divisions = units.map((...args) => extendDivision(...args, axisType)); for (let d of divisions) { const overlappingUnits = divisions.filter((u) => @@ -74,12 +77,6 @@ export function preprocessUnits( } } - // TODO: we may want to re-enable this simpler processing for sections, - // but we want it to be less universally applied. - // if (axisType != ColumnAxisType.AGE) { - // return preprocessSectionUnits(divisions, axisType); - // } - // return divisions; } @@ -89,8 +86,15 @@ function extendDivision( divisions: UnitLong[], axisType: ColumnAxisType = ColumnAxisType.AGE, ): ExtUnit { + // TODO: make this configurable + let tolerance = 0.001; // 1 kyr tolerance for age columns + if (axisType != ColumnAxisType.AGE) { + tolerance = 0.01; // 1cm tolerance for height/depth columns + } + const overlappingUnits = divisions.filter( - (d) => d.unit_id != unit.unit_id && unitsOverlap(unit, d, axisType), + (d) => + d.unit_id != unit.unit_id && unitsOverlap(unit, d, axisType, tolerance), ); const u_pos = getUnitHeightRange(unit, axisType); const bottomOverlap = overlappingUnits.some((d) => { @@ -235,38 +239,14 @@ export function mergeOverlappingSections( return newSections; } -function preprocessSectionUnits( - units: UnitLong[], - axisType: ColumnAxisType = ColumnAxisType.DEPTH, -): ExtUnit[] { - /** Preprocess units for a "section" column type, which is guaranteed to be simpler. */ - // We have to assume the units are ordered... - let thickness = 0; - return units.map((unit, i) => { - let u1 = preprocessSectionUnit(unit, i, units, thickness, axisType); - thickness += Math.abs(u1.t_pos - u1.b_pos); - return u1; - }); -} - -function preprocessSectionUnit( - unit: UnitLong, - i: number, - units: UnitLong[], - accumulatedThickness: number = 0, - axisType: ColumnAxisType = ColumnAxisType.DEPTH, -): ExtUnit { +function preprocessSectionUnit(unit: UnitLong): UnitLong { /** Preprocess a single unit for a "section" column type. - * No provision for overlapping units. + * This mostly handles vagaries of eODP-style columns. * */ let b_pos = unit.b_pos; let t_pos = unit.t_pos; - if (b_pos == t_pos && axisType == ColumnAxisType.ORDINAL) { - t_pos = t_pos - 1; - } - let unit_name = unit.unit_name; // eODP columns sometimes have overlapping core sections, which are encoded in the name field @@ -285,7 +265,5 @@ function preprocessSectionUnit( b_pos: ensureRealFloat(b_pos), t_pos: ensureRealFloat(t_pos), unit_name, - bottomOverlap: false, - overlappingUnits: [], }; } diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index f755f433..90c539f9 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -26,6 +26,8 @@ export interface PrepareColumnOptions extends ColumnScaleOptions { axisType: ColumnAxisType; t_age?: number; b_age?: number; + t_pos?: number; + b_pos?: number; mergeSections?: MergeSectionsMode; collapseSmallUnconformities?: boolean | number; } @@ -61,6 +63,8 @@ export function prepareColumnUnits( const { t_age, b_age, + t_pos, + b_pos, mergeSections = MergeSectionsMode.OVERLAPPING, axisType, unconformityHeight, @@ -70,7 +74,11 @@ export function prepareColumnUnits( /** Prototype filtering to age range */ let units1 = units.filter((d) => { // Filter units by t_age and b_age, inclusive - return agesOverlap(d, { t_age, b_age }); + if (axisType == ColumnAxisType.AGE) { + return agesOverlap(d, { t_age, b_age }); + } else { + return unitsOverlap(d, { t_pos, b_pos } as any, axisType); + } }); let sections0: SectionInfo[]; diff --git a/packages/column-views/stories/sections.stories.ts b/packages/column-views/stories/sections.stories.ts index d3d8b1de..028222f3 100644 --- a/packages/column-views/stories/sections.stories.ts +++ b/packages/column-views/stories/sections.stories.ts @@ -50,6 +50,26 @@ export const eODPColumnV2: Story = { }, }; +export const eODPColumnNoOverlappingUnits: Story = { + args: { + id: 5248, + inProcess: true, + maxInternalColumns: 1, + pixelScale: 10, + }, +}; + +export const eODPColumnFilteredToHeightRange: Story = { + args: { + id: 5248, + inProcess: true, + maxInternalColumns: 1, + pixelScale: 10, + t_pos: 15, + b_pos: 22, + }, +}; + export const OrdinalPosition: Story = { args: { id: 432, diff --git a/packages/stratigraphy-utils/src/age-ranges.ts b/packages/stratigraphy-utils/src/age-ranges.ts index d9420742..ddfd648d 100644 --- a/packages/stratigraphy-utils/src/age-ranges.ts +++ b/packages/stratigraphy-utils/src/age-ranges.ts @@ -43,6 +43,8 @@ function convertToForwardOrdinal(a: AgeRange): AgeRange { * expressed as negative numbers. This assists with intuitive ordering * in certain cases. */ + a = [Number(a[0]), Number(a[1])]; + if (a[0] < a[1]) { // Already in forward ordinal form return a; From be63988c1673b3094aaa4a88665dfa9ad041a66e Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 22 Nov 2025 05:36:10 -0600 Subject: [PATCH 03/46] Updated unit height scale --- .../column-views/src/prepare-units/helpers.ts | 79 ++++++++++++++++--- .../column-views/src/prepare-units/index.ts | 67 +++++++++++----- packages/column-views/src/units/boxes.ts | 2 - .../column-views/stories/sections.stories.ts | 2 +- 4 files changed, 116 insertions(+), 34 deletions(-) diff --git a/packages/column-views/src/prepare-units/helpers.ts b/packages/column-views/src/prepare-units/helpers.ts index 010d3dfb..102412d0 100644 --- a/packages/column-views/src/prepare-units/helpers.ts +++ b/packages/column-views/src/prepare-units/helpers.ts @@ -25,6 +25,8 @@ export interface SectionInfo /** A time-bounded part of a single stratigraphic column. */ section_id: number | number[]; units: T[]; + b_pos?: number; + t_pos?: number; } export interface ExtUnit extends UnitLong { @@ -45,8 +47,6 @@ export function preprocessUnits( /** Preprocess units to add overlapping units and columns. */ let units = section.units; - units = units.map(preprocessSectionUnit); - let divisions = units.map((...args) => extendDivision(...args, axisType)); for (let d of divisions) { const overlappingUnits = divisions.filter((u) => @@ -66,8 +66,8 @@ export function preprocessUnits( } // If unit overlaps the edges of a section, set the clip positions + const [b_pos, t_pos] = getUnitHeightRange(d, axisType); if (axisType == ColumnAxisType.AGE) { - const [b_pos, t_pos] = getUnitHeightRange(d, axisType); if (b_pos > section.b_age) { d.b_clip_pos = section.b_age; } @@ -75,6 +75,22 @@ export function preprocessUnits( d.t_clip_pos = section.t_age; } } + if (axisType == ColumnAxisType.DEPTH) { + if (b_pos > section.b_pos) { + d.b_clip_pos = section.b_pos; + } + if (t_pos < section.t_pos) { + d.t_clip_pos = section.t_pos; + } + } + // if (axisType == ColumnAxisType.HEIGHT) { + // if (b_pos < section.b_pos) { + // d.b_clip_pos = section.b_pos; + // } + // if (t_pos > section.t_pos) { + // d.t_clip_pos = section.t_pos; + // } + // } } return divisions; @@ -131,13 +147,15 @@ export function groupUnitsIntoSections( const groups1 = groups.map(([section_id, sectionUnits]) => { const [b_age, t_age] = getSectionAgeRange(sectionUnits); + const [b_pos, t_pos] = getSectionPosRange(sectionUnits, axisType); + // sort units by position sectionUnits.sort(unitComparator); - return { section_id, t_age, b_age, units: sectionUnits }; + return { section_id, t_age, b_age, b_pos, t_pos, units: sectionUnits }; }); // Sort sections by increasing top age, then increasing bottom age. // Sections have no relative ordinal position other than age... - const compareSections = createUnitSorter(ColumnAxisType.AGE) as ( + const compareSections = createUnitSorter(axisType) as ( a: StratigraphicPackage, b: StratigraphicPackage, ) => number; @@ -189,23 +207,58 @@ function groupUnitsIntoSectionByOverlap( return sectionList.map((section, i) => { const [b_age, t_age] = getSectionAgeRange(section.units); + const [b_pos, t_pos] = getSectionPosRange(section.units, axisType); return { // Negative section IDs are used to indicate that these are synthetic sections section_id: -i, - t_age: t_age, - b_age: b_age, + t_age, + b_age, + t_pos, + b_pos, units: section.units as T[], }; }); } +export function getSectionPosRange( + units: BaseUnit[], + axisType: ColumnAxisType, +): [number, number] { + /** Get the overall position range of a set of units. */ + const t_positions = units.map((d) => { + switch (axisType) { + case ColumnAxisType.AGE: + return d.t_age; + case ColumnAxisType.DEPTH: + case ColumnAxisType.HEIGHT: + case ColumnAxisType.ORDINAL: + return d.t_pos; + default: + throw new Error(`Unknown axis type: ${axisType}`); + } + }); + const b_positions = units.map((d) => { + switch (axisType) { + case ColumnAxisType.AGE: + return d.b_age; + case ColumnAxisType.DEPTH: + case ColumnAxisType.HEIGHT: + case ColumnAxisType.ORDINAL: + return d.b_pos; + default: + throw new Error(`Unknown axis type: ${axisType}`); + } + }); + if (axisType == ColumnAxisType.AGE || axisType == ColumnAxisType.DEPTH) { + return [Math.max(...b_positions), Math.min(...t_positions)]; + } else { + return [Math.min(...b_positions), Math.max(...t_positions)]; + } +} + export function getSectionAgeRange(units: BaseUnit[]): [number, number] { /** Get the overall age range of a set of units. */ - const t_ages = units.map((d) => d.t_age); - const b_ages = units.map((d) => d.b_age); - const t_age = Math.min(...t_ages); - const b_age = Math.max(...b_ages); - return [b_age, t_age]; + return getSectionPosRange(units, ColumnAxisType.AGE); } export function mergeOverlappingSections( @@ -239,7 +292,7 @@ export function mergeOverlappingSections( return newSections; } -function preprocessSectionUnit(unit: UnitLong): UnitLong { +export function preprocessSectionUnit(unit: UnitLong): UnitLong { /** Preprocess a single unit for a "section" column type. * This mostly handles vagaries of eODP-style columns. * */ diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 90c539f9..ee9d45ea 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -1,7 +1,9 @@ import { getSectionAgeRange, + getSectionPosRange, groupUnitsIntoSections, mergeOverlappingSections, + preprocessSectionUnit, preprocessUnits, } from "./helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; @@ -71,8 +73,12 @@ export function prepareColumnUnits( collapseSmallUnconformities = false, } = options; + // Start by ensuring that ages and positions are numbers + // also set up some values for eODP-style columns + let units1 = units.map(preprocessSectionUnit); + /** Prototype filtering to age range */ - let units1 = units.filter((d) => { + units1 = units1.filter((d) => { // Filter units by t_age and b_age, inclusive if (axisType == ColumnAxisType.AGE) { return agesOverlap(d, { t_age, b_age }); @@ -82,25 +88,40 @@ export function prepareColumnUnits( }); let sections0: SectionInfo[]; - if ( - mergeSections == MergeSectionsMode.ALL && - axisType != ColumnAxisType.ORDINAL - ) { - // For the "merge sections" mode, we need to create a single section - const [b_unit_age, t_unit_age] = getSectionAgeRange(units1); + if (axisType == ColumnAxisType.AGE) { + if ( + mergeSections == MergeSectionsMode.ALL && + axisType == ColumnAxisType.AGE + ) { + // For the "merge sections" mode, we need to create a single section + const [b_unit_age, t_unit_age] = getSectionAgeRange(units1); + sections0 = [ + { + section_id: 0, + /** + * If ages limits are directly specified, use them to define the section bounds. + * */ + t_age: t_age ?? t_unit_age, + b_age: b_age ?? b_unit_age, + units: units1, + }, + ]; + } else { + sections0 = groupUnitsIntoSections(units1, axisType); + } + } else { + const [b_unit_pos, t_unit_pos] = getSectionPosRange(units1, axisType); + const [t_age, b_age] = getSectionAgeRange(units1); sections0 = [ { section_id: 0, - /** - * If ages limits are directly specified, use them to define the section bounds. - * */ - t_age: t_age ?? t_unit_age, - b_age: b_age ?? b_unit_age, + t_pos: t_pos ?? t_unit_pos, + b_pos: b_pos ?? b_unit_pos, + t_age, + b_age, units: units1, }, ]; - } else { - sections0 = groupUnitsIntoSections(units1, axisType); } /** Merging overlapping sections really only makes sense for age/height/depth @@ -110,7 +131,7 @@ export function prepareColumnUnits( let sections = sections0; if ( mergeSections == MergeSectionsMode.OVERLAPPING && - axisType != ColumnAxisType.ORDINAL + axisType == ColumnAxisType.AGE ) { sections = mergeOverlappingSections(sections); } @@ -121,11 +142,21 @@ export function prepareColumnUnits( * are correctly limited to the t_age and b_age applied to the overall column. */ sections = sections.map((section) => { - const { t_age, b_age } = section; + let { t_pos, b_pos } = section; + if (axisType == ColumnAxisType.DEPTH) { + t_pos = Math.max(section.t_pos, options.t_pos ?? -Infinity); + b_pos = Math.min(section.b_pos, options.b_pos ?? Infinity); + } else if (axisType == ColumnAxisType.HEIGHT) { + t_pos = Math.max(section.t_pos, options.t_pos ?? -Infinity); + b_pos = Math.min(section.b_pos, options.b_pos ?? Infinity); + } + return { ...section, - t_age: Math.max(t_age, options.t_age ?? -Infinity), - b_age: Math.min(b_age, options.b_age ?? Infinity), + t_age: Math.max(section.t_age, options.t_age ?? -Infinity), + b_age: Math.min(section.b_age, options.b_age ?? Infinity), + t_pos, + b_pos, }; }); diff --git a/packages/column-views/src/units/boxes.ts b/packages/column-views/src/units/boxes.ts index 74e7f141..0a935539 100644 --- a/packages/column-views/src/units/boxes.ts +++ b/packages/column-views/src/units/boxes.ts @@ -162,8 +162,6 @@ function Unit(props: UnitProps) { const [ref, selected, onClick] = useUnitSelectionTarget(d); - //const key = `unit-${d.unit_id}`; - return h( "g.unit", { diff --git a/packages/column-views/stories/sections.stories.ts b/packages/column-views/stories/sections.stories.ts index 028222f3..bcfc6bd7 100644 --- a/packages/column-views/stories/sections.stories.ts +++ b/packages/column-views/stories/sections.stories.ts @@ -66,7 +66,7 @@ export const eODPColumnFilteredToHeightRange: Story = { maxInternalColumns: 1, pixelScale: 10, t_pos: 15, - b_pos: 22, + b_pos: 25, }, }; From 84d2ceef505ce6d96b7ba537957094832a1d399a Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 22 Nov 2025 05:58:53 -0600 Subject: [PATCH 04/46] Updated composite scale --- .../src/prepare-units/composite-scale.ts | 7 ++- .../column-views/src/prepare-units/index.ts | 46 ++++++++----------- 2 files changed, 25 insertions(+), 28 deletions(-) diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index d581836e..66bc0689 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -113,6 +113,7 @@ export function buildCompositeScaleInfo( export function finalizeSectionHeights( sections: SectionInfoWithScale[], unconformityHeight: number, + axisType: ColumnAxisType, ): CompositeColumnData { /** Finalize the heights of sections, including the heights of unconformities * between them. @@ -159,11 +160,13 @@ function addScaleToSection( group: SectionInfo, opts: ColumnScaleOptions, ): SectionInfoWithScale { - const { t_age, b_age, units } = group; + const { t_age, b_age, t_pos, b_pos, units } = group; let _range = null; // if t_age and b_age are set for a group, use them to define the range... - if (t_age != null && b_age != null && opts.axisType == ColumnAxisType.AGE) { + if (opts.axisType == ColumnAxisType.AGE) { _range = [b_age, t_age]; + } else { + _range = [b_pos, t_pos]; } const scaleInfo = buildSectionScale(units, { diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index ee9d45ea..feb01688 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -87,41 +87,34 @@ export function prepareColumnUnits( } }); + let mergeMode = mergeSections; + if (axisType != ColumnAxisType.AGE) { + // For non-age columns, we always merge sections. + // This is because the "groupUnitsIntoSections" function is not well-defined + // for non-age columns. + mergeMode = MergeSectionsMode.ALL; + } + let sections0: SectionInfo[]; - if (axisType == ColumnAxisType.AGE) { - if ( - mergeSections == MergeSectionsMode.ALL && - axisType == ColumnAxisType.AGE - ) { - // For the "merge sections" mode, we need to create a single section - const [b_unit_age, t_unit_age] = getSectionAgeRange(units1); - sections0 = [ - { - section_id: 0, - /** - * If ages limits are directly specified, use them to define the section bounds. - * */ - t_age: t_age ?? t_unit_age, - b_age: b_age ?? b_unit_age, - units: units1, - }, - ]; - } else { - sections0 = groupUnitsIntoSections(units1, axisType); - } - } else { + if (mergeMode == MergeSectionsMode.ALL) { + // For the "merge sections" mode, we need to create a single section const [b_unit_pos, t_unit_pos] = getSectionPosRange(units1, axisType); - const [t_age, b_age] = getSectionAgeRange(units1); + const [b_unit_age, t_unit_age] = getSectionAgeRange(units1); sections0 = [ { section_id: 0, + /** + * If ages limits are directly specified, use them to define the section bounds. + * */ t_pos: t_pos ?? t_unit_pos, b_pos: b_pos ?? b_unit_pos, - t_age, - b_age, + t_age: t_age ?? t_unit_age, + b_age: b_age ?? b_unit_age, units: units1, }, ]; + } else { + sections0 = groupUnitsIntoSections(units1, axisType); } /** Merging overlapping sections really only makes sense for age/height/depth @@ -180,9 +173,10 @@ export function prepareColumnUnits( } /** Prepare section scale information using groups */ - const { totalHeight, sections: sections2 } = finalizeSectionHeights( + let { totalHeight, sections: sections2 } = finalizeSectionHeights( sectionsWithScales, unconformityHeight, + axisType, ); /** For each section, find units that are overlapping. From 6f43eeb816a93e10798c47da3076342bd8fec212 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 22 Nov 2025 06:04:31 -0600 Subject: [PATCH 05/46] Separate out age limitation --- .../column-views/src/prepare-units/index.ts | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index feb01688..4dfd070a 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -106,10 +106,10 @@ export function prepareColumnUnits( /** * If ages limits are directly specified, use them to define the section bounds. * */ - t_pos: t_pos ?? t_unit_pos, - b_pos: b_pos ?? b_unit_pos, - t_age: t_age ?? t_unit_age, - b_age: b_age ?? b_unit_age, + t_pos: t_unit_pos, + b_pos: b_unit_pos, + t_age: t_unit_age, + b_age: b_unit_age, units: units1, }, ]; @@ -117,6 +117,20 @@ export function prepareColumnUnits( sections0 = groupUnitsIntoSections(units1, axisType); } + // Limit sections to the range specified by t_age/b_age or t_pos/b_pos global options + for (let section of sections0) { + if (axisType == ColumnAxisType.AGE) { + section.t_age = Math.max(section.t_age, t_age ?? -Infinity); + section.b_age = Math.min(section.b_age, b_age ?? Infinity); + } else if (axisType == ColumnAxisType.DEPTH) { + section.t_pos = Math.max(section.t_pos, t_pos ?? -Infinity); + section.b_pos = Math.min(section.b_pos, b_pos ?? Infinity); + } else if (axisType == ColumnAxisType.HEIGHT) { + section.t_pos = Math.max(section.t_pos, t_pos ?? -Infinity); + section.b_pos = Math.min(section.b_pos, b_pos ?? Infinity); + } + } + /** Merging overlapping sections really only makes sense for age/height/depth * columns. Ordinal columns are numbered by section so merging them * results in collisions. From b2c1f8f5d3af3c02de096113fc3eb0bcbaaa925b Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 22 Nov 2025 20:26:21 -0600 Subject: [PATCH 06/46] Composite scale seems to work for height-based columns --- .../src/prepare-units/composite-scale.ts | 24 +++++++++- .../column-views/src/prepare-units/helpers.ts | 39 +++++++++------- .../column-views/src/prepare-units/index.ts | 45 ++++++------------- 3 files changed, 60 insertions(+), 48 deletions(-) diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 66bc0689..c8d588b6 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -369,14 +369,34 @@ export function collapseUnconformitiesByPixelHeight( currentSection = nextSection; continue; } - const dAge = Math.abs(nextSection.t_age - currentSection.b_age); + let dAge: number; + if (opts.axisType !== ColumnAxisType.AGE) { + dAge = Math.abs(nextSection.t_pos - currentSection.b_pos); + } else { + dAge = Math.abs(nextSection.t_age - currentSection.b_age); + } + const pxHeight = dAge * Math.max( currentSection.scaleInfo.pixelScale, nextSection.scaleInfo.pixelScale, ); + if (pxHeight < threshold) { + let t_pos: number; + let b_pos: number; + if ( + opts.axisType == ColumnAxisType.AGE || + opts.axisType == ColumnAxisType.DEPTH + ) { + t_pos = Math.min(currentSection.t_pos, nextSection.t_pos); + b_pos = Math.max(currentSection.b_pos, nextSection.b_pos); + } else { + t_pos = Math.max(currentSection.t_pos, nextSection.t_pos); + b_pos = Math.min(currentSection.b_pos, nextSection.b_pos); + } + // We need to merge the sections const compositeSection0: SectionInfo = { units: [...currentSection.units, ...nextSection.units], @@ -386,6 +406,8 @@ export function collapseUnconformitiesByPixelHeight( ], t_age: Math.min(currentSection.t_age, nextSection.t_age), b_age: Math.max(currentSection.b_age, nextSection.b_age), + t_pos, + b_pos, }; const compositeSection = addScaleToSection(compositeSection0, opts); diff --git a/packages/column-views/src/prepare-units/helpers.ts b/packages/column-views/src/prepare-units/helpers.ts index 102412d0..61143e17 100644 --- a/packages/column-views/src/prepare-units/helpers.ts +++ b/packages/column-views/src/prepare-units/helpers.ts @@ -2,14 +2,16 @@ import type { BaseUnit, UnitLong } from "@macrostrat/api-types"; import { group } from "d3-array"; import { ColumnAxisType } from "@macrostrat/column-components"; import { - unitsOverlap, - getUnitHeightRange, createUnitSorter, ensureArray, ensureRealFloat, - PossiblyClippedUnit, + getUnitHeightRange, + unitsOverlap, } from "./utils"; -import { compareAgeRanges } from "@macrostrat/stratigraphy-utils"; +import { + AgeRangeRelationship, + compareAgeRanges, +} from "@macrostrat/stratigraphy-utils"; const dt = 0.001; @@ -131,14 +133,10 @@ function extendDivision( }; } -export function groupUnitsIntoSections( +export function groupUnitsIntoSectionsBySectionID( units: T[], axisType: ColumnAxisType = ColumnAxisType.AGE, ): SectionInfo[] { - if (axisType != ColumnAxisType.AGE) { - return groupUnitsIntoSectionByOverlap(units, axisType); - } - /** Group units into sections by section_id. * This works for large-scale Macrostrat columns, where units are grouped by section_id. * */ @@ -169,7 +167,7 @@ interface WorkingSection { heightRange?: [number, number]; } -function groupUnitsIntoSectionByOverlap( +export function groupUnitsIntoSectionsByOverlap( units: T[], axisType: ColumnAxisType = ColumnAxisType.AGE, ): SectionInfo[] { @@ -183,8 +181,10 @@ function groupUnitsIntoSectionByOverlap( for (const unit of units) { // Check if the unit overlaps with any existing section const heightRange = getUnitHeightRange(unit, axisType); - let section: WorkingSection | undefined = sectionList.find((s) => - compareAgeRanges(heightRange, s.heightRange), + let section: WorkingSection | undefined = sectionList.find( + (s) => + compareAgeRanges(heightRange, s.heightRange) !== + AgeRangeRelationship.Disjoint, ); if (section == null) { // No overlap, create a new section @@ -196,10 +196,17 @@ function groupUnitsIntoSectionByOverlap( // Overlap, merge the unit into the section section.units.push(unit); // Update the height range - section.heightRange = [ - Math.max(section.heightRange[0], heightRange[0]), - Math.min(section.heightRange[1], heightRange[1]), - ]; + if (axisType == ColumnAxisType.DEPTH || axisType == ColumnAxisType.AGE) { + section.heightRange = [ + Math.max(section.heightRange[0], heightRange[0]), + Math.min(section.heightRange[1], heightRange[1]), + ]; + } else { + section.heightRange = [ + Math.min(section.heightRange[0], heightRange[0]), + Math.max(section.heightRange[1], heightRange[1]), + ]; + } } } // We should have a section for each unit, now we can convert to SectionInfo diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 4dfd070a..5d304889 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -1,7 +1,8 @@ import { getSectionAgeRange, getSectionPosRange, - groupUnitsIntoSections, + groupUnitsIntoSectionsByOverlap, + groupUnitsIntoSectionsBySectionID, mergeOverlappingSections, preprocessSectionUnit, preprocessUnits, @@ -22,7 +23,7 @@ import type { SectionInfo } from "./helpers"; import { agesOverlap, getUnitHeightRange, unitsOverlap } from "./utils"; export * from "./utils"; -export { preprocessUnits, groupUnitsIntoSections }; +export { preprocessUnits }; export interface PrepareColumnOptions extends ColumnScaleOptions { axisType: ColumnAxisType; @@ -88,12 +89,12 @@ export function prepareColumnUnits( }); let mergeMode = mergeSections; - if (axisType != ColumnAxisType.AGE) { - // For non-age columns, we always merge sections. - // This is because the "groupUnitsIntoSections" function is not well-defined - // for non-age columns. - mergeMode = MergeSectionsMode.ALL; - } + // if (axisType != ColumnAxisType.AGE) { + // // For non-age columns, we always merge sections. + // // This is because the "groupUnitsIntoSections" function is not well-defined + // // for non-age columns. + // mergeMode = MergeSectionsMode.ALL; + // } let sections0: SectionInfo[]; if (mergeMode == MergeSectionsMode.ALL) { @@ -113,8 +114,10 @@ export function prepareColumnUnits( units: units1, }, ]; + } else if (axisType == ColumnAxisType.AGE) { + sections0 = groupUnitsIntoSectionsBySectionID(units1, axisType); } else { - sections0 = groupUnitsIntoSections(units1, axisType); + sections0 = groupUnitsIntoSectionsByOverlap(units1, axisType); } // Limit sections to the range specified by t_age/b_age or t_pos/b_pos global options @@ -145,34 +148,14 @@ export function prepareColumnUnits( // Filter out undefined sections just in case sections = sections.filter((d) => d != null); - /* Now that we are done merging sections, we can ensure that our sections - * are correctly limited to the t_age and b_age applied to the overall column. - */ - sections = sections.map((section) => { - let { t_pos, b_pos } = section; - if (axisType == ColumnAxisType.DEPTH) { - t_pos = Math.max(section.t_pos, options.t_pos ?? -Infinity); - b_pos = Math.min(section.b_pos, options.b_pos ?? Infinity); - } else if (axisType == ColumnAxisType.HEIGHT) { - t_pos = Math.max(section.t_pos, options.t_pos ?? -Infinity); - b_pos = Math.min(section.b_pos, options.b_pos ?? Infinity); - } - - return { - ...section, - t_age: Math.max(section.t_age, options.t_age ?? -Infinity), - b_age: Math.min(section.b_age, options.b_age ?? Infinity), - t_pos, - b_pos, - }; - }); - /* Compute pixel scales etc. for sections * We need to do this now to determine which unconformities * are small enough to collapse. */ let sectionsWithScales = computeSectionHeights(sections, options); + // Collapse small unconformities in pixel height space + // TODO: this doesn't seem to work properly for non-age columns? if (collapseSmallUnconformities ?? false) { let threshold = unconformityHeight ?? 30; if (typeof collapseSmallUnconformities == "number") { From 21d2f6f4c34b032707192de2a5d241f6ba4c2966 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sat, 22 Nov 2025 22:30:51 -0600 Subject: [PATCH 07/46] Updated unconformity info --- packages/column-views/src/section.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 16d2df32..2a9a5925 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -16,6 +16,7 @@ import { MacrostratColumnProvider, } from "./data-provider"; import { Duration } from "./unit-details"; +import { Value } from "@macrostrat/data-components"; const h = hyper.styled(styles); @@ -84,6 +85,7 @@ function SectionUnitsColumn(props: SectionSharedProps) { unitComponentProps, clipUnits, maxInternalColumns, + axisType, unconformityLabels = true, } = props; @@ -127,6 +129,7 @@ function SectionUnitsColumn(props: SectionSharedProps) { h.if(unconformityLabels)(UnconformityLabels, { width, sections: scaleData, + axisType, }), ]); } @@ -257,6 +260,7 @@ export function UnconformityLabels(props: { sections: PackageScaleLayoutData[]; className?: string; }) { + const { axisType } = useMacrostratColumnData(); const { width, sections, className } = props; return h( @@ -273,6 +277,7 @@ export function UnconformityLabels(props: { const upperAge = lastGroup?.domain[0]; const lowerAge = scaleInfo.domain[1]; return h(Unconformity, { + axisType, upperAge, lowerAge, style: { @@ -285,7 +290,7 @@ export function UnconformityLabels(props: { ); } -function Unconformity({ upperAge, lowerAge, style }) { +function Unconformity({ upperAge, lowerAge, style, axisType }) { if (upperAge == null || lowerAge == null) { return null; } @@ -303,7 +308,15 @@ function Unconformity({ upperAge, lowerAge, style }) { className = "small"; } + let val: ReactNode; + if (axisType === ColumnAxisType.DEPTH || axisType === ColumnAxisType.HEIGHT) { + const _txt = ageGap.toLocaleString("en-US", { maximumFractionDigits: 2 }); + val = h(Value, { value: _txt, unit: "m" }); + } else { + val = h(Duration, { value: ageGap }); + } + return h("div.unconformity", { style, className }, [ - h("div.unconformity-text", h(Duration, { value: ageGap })), + h("div.unconformity-text", val), ]); } From f7d1761eff07783d0c02d160d376b6b6a686ab9f Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sun, 23 Nov 2025 05:19:39 -0600 Subject: [PATCH 08/46] Add PBDB occurrences --- packages/column-views/src/column.ts | 9 +- .../column-views/src/facets/fossils/index.ts | 32 +++- .../src/facets/fossils/provider.ts | 137 +++++++++++++++--- packages/column-views/src/section.ts | 1 - .../stories/facets/pbdb.stories.ts | 24 ++- .../column-views/stories/sections.stories.ts | 10 ++ 6 files changed, 177 insertions(+), 36 deletions(-) diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 5fa93760..153a84b0 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -93,6 +93,13 @@ export function Column(props: ColumnProps) { const ref = useRef(); // Selected item position + /* Make pixelScale and targetUnitHeight mutually exclusive. PixelScale implies + * standardization of scales in all sections */ + let _targetUnitHeight = targetUnitHeight; + if (pixelScale != null) { + _targetUnitHeight = null; + } + const { sections, units, totalHeight } = usePreparedColumnUnits(rawUnits, { axisType, t_age, @@ -100,7 +107,7 @@ export function Column(props: ColumnProps) { t_pos, b_pos, mergeSections, - targetUnitHeight, + targetUnitHeight: _targetUnitHeight, unconformityHeight, pixelScale, minPixelScale, diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index 39f2e047..fccbab84 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -1,8 +1,12 @@ import { getUnitHeightRange } from "../../prepare-units"; import { useMacrostratColumnData } from "../../data-provider"; import hyper from "@macrostrat/hyper"; -import { PBDBCollection, useFossilData } from "./provider"; -import { useMacrostratUnits } from "../../data-provider"; +import { + FossilDataType, + PBDBCollection, + PBDBOccurrence, + useFossilData, +} from "./provider"; import { ColumnNotes } from "../../notes"; import { useMemo } from "react"; import type { IUnit } from "../../units"; @@ -11,6 +15,8 @@ import { useCallback } from "react"; const h = hyper.styled(styles); +export { FossilDataType }; + interface FossilItemProps { note: { data: PBDBCollection[]; @@ -65,20 +71,31 @@ export function TruncatedList({ ]); } -function PBDBCollectionLink({ data }: { data: PBDBCollection }) { +function PBDBCollectionLink({ + data, +}: { + data: PBDBCollection | PBDBOccurrence; +}) { + /** A link to a PBDB collection that handles either an occurrence or collection object */ return h( "a.link-id", { href: `https://paleobiodb.org/classic/basicCollectionSearch?collection_no=${data.cltn_id}`, }, - data.cltn_name, + data.best_name ?? data.cltn_name, ); } const matchingUnit = (dz) => (d) => d.unit_id == dz.unit_id; -export function PBDBFossilsColumn({ columnID, color = "magenta" }) { - const data = useFossilData({ col_id: columnID }); +export function PBDBFossilsColumn({ + columnID, + type = FossilDataType.Collections, +}: { + columnID: number; + type: FossilDataType; +}) { + const data = useFossilData({ col_id: columnID, type }); const { axisType, units } = useMacrostratColumnData(); @@ -119,11 +136,10 @@ export function PBDBFossilsColumn({ columnID, color = "magenta" }) { const noteComponent = useMemo(() => { return (props) => { return h(FossilInfo, { - color, ...props, }); }; - }, [width, color]); + }, [width]); if (data == null || units == null) return null; diff --git a/packages/column-views/src/facets/fossils/provider.ts b/packages/column-views/src/facets/fossils/provider.ts index b9dff27e..057f8c11 100644 --- a/packages/column-views/src/facets/fossils/provider.ts +++ b/packages/column-views/src/facets/fossils/provider.ts @@ -1,10 +1,14 @@ import { group } from "d3-array"; -import { createAPIContext, useAPIResult } from "@macrostrat/ui-components"; +import { + createAPIContext, + useAPIResult, + useAsyncMemo, +} from "@macrostrat/ui-components"; const responseUnwrapper = (d) => d.records; const pbdbAPIContext = createAPIContext({ - baseURL: "https://training.paleobiodb.org/data1.2", + baseURL: "https://paleobiodb.org/data1.2", unwrapResponse: responseUnwrapper, }); @@ -26,10 +30,13 @@ export function usePBDBFossilData( }); } -export interface PBDBCollection { +export interface PBDBIdentifier { unit_id: number; col_id: number; cltn_id: number; +} + +export interface PBDBCollection extends PBDBIdentifier { cltn_name: string; pbdb_occs: number; t_age: number; @@ -37,44 +44,126 @@ export interface PBDBCollection { [key: string]: any; // Allow for additional properties } -function useMacrostratFossilData({ col_id }): PBDBCollection[] | null { - return useAPIResult("/fossils", { col_id }); +export interface PBDBOccurrence extends PBDBIdentifier { + occ_id: number; + taxon_name: string; + best_name: string; + [key: string]: any; // Allow for additional properties +} + +export function useFossilData({ col_id, type = FossilDataType.Collections }) { + // Fossil links are stored in both Macrostrat and PBDB, depending on how the link was assembled. Here + // we create a unified view of data over both sources. + return useAsyncMemo(async () => { + if (col_id == null) return null; + return await fetchFossilData(col_id, type); + }, [col_id, type]); +} + +async function fetchMacrostratFossilData( + col_id: number, + type: FossilDataType, +): Promise { + if (type !== FossilDataType.Collections) { + // Macrostrat API only supports collections + return []; + } + + // Fetch fossil collections linked to columns from the Macrostrat API + const resp = await fetch( + `https://macrostrat.org/api/fossils?col_id=${col_id}`, + ); + const res = await resp.json(); + // Create collections from Macrostrat data + return res.success.data; +} + +async function fetchPDBDFossilData( + col_id: number, + type: FossilDataType, +): Promise { + const resp = await fetch( + `https://paleobiodb.org/data1.2/${type}/list.json?ms_column=${col_id}&show=mslink,full`, + ); + const res = await resp.json(); + return res.records.map( + type == FossilDataType.Collections + ? createMacrostratCollection + : preprocessOccurrence, + ); +} + +async function fetchFossilData(colID: number, type: FossilDataType) { + const [macrostratData, pbdbData] = await Promise.all([ + fetchMacrostratFossilData(colID, type), + fetchPDBDFossilData(colID, type), + ]); + + const data = [...macrostratData, ...pbdbData]; + + return group(data, (d) => d.unit_id); +} + +function preprocessOccurrence(d): PBDBOccurrence { + /* Preprocess data for an occurrence into a Macrostrat-like format */ + // Standardize names of Macrostrat units and columns + const unit_id = parseInt(d.msu.replace(/^\w+:/, "")); + const col_id = parseInt(d.msc.replace(/^\w+:/, "")); + + // taxon names may be stored in different fields + const occ_id = parseInt(d.oid.replace(/^occ:/, "")); + const cltn_id = parseInt(d.cid.replace(/^col:/, "")); + + return { + ...d, + unit_id, + col_id, + taxon_name: d.tna, + best_name: d.idn ?? d.tna, + occ_id, + cltn_id, + cltn_name: d.nam, + }; } function createMacrostratCollection(d): PBDBCollection { + /* Preprocess data for a collection into a Macrostrat-like format */ let unit_id = null; let col_id = null; // Standardize names of Macrostrat units and columns - if (d.msu !== null) { + if (d.msu != null) { unit_id = parseInt(d.msu.replace(/^\w+:/, "")); } - if (d.msc !== null) { + if (d.msc != null) { col_id = parseInt(d.msc.replace(/^\w+:/, "")); } + // taxon names may be stored in different fields + let taxon_name = d.tna; + let occ_id = null; + if (d.oid != null && d.oid.startsWith("occ:")) { + occ_id = parseInt(d.oid.replace(/^occ:/, "")); + } + if (d.idn != null) { + taxon_name = d.idn; + } + + let cltn_id = d.cltn_id; + if (d.oid != null && d.oid.startsWith("col:")) { + cltn_id = parseInt(d.oid.replace(/^col:/, "")); + } else if (d.cid != null && d.cid.startsWith("col:")) { + cltn_id = parseInt(d.cid.replace(/^col:/, "")); + } + return { ...d, unit_id, col_id, - cltn_id: parseInt(d.oid.replace(/^col:/, "")), + taxon_name, + occ_id, + cltn_id, cltn_name: d.nam, t_age: d.t_age, b_age: d.b_age, }; } - -export function useFossilData({ col_id }) { - // Fossil links are stored in both Macrostrat and PBDB, depending on how the link was assembled. Here - // we create a unified view of data over both sources. - - const r1 = usePBDBFossilData(FossilDataType.Collections, { col_id }); - - const r2 = useMacrostratFossilData({ col_id }); - - if (r1 == null || r2 == null) return null; - const r1a = r1.map(createMacrostratCollection); - - const data = [...r1a, ...r2]; - - return group(data, (d) => d.unit_id); -} diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 2a9a5925..42ff8ef0 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -129,7 +129,6 @@ function SectionUnitsColumn(props: SectionSharedProps) { h.if(unconformityLabels)(UnconformityLabels, { width, sections: scaleData, - axisType, }), ]); } diff --git a/packages/column-views/stories/facets/pbdb.stories.ts b/packages/column-views/stories/facets/pbdb.stories.ts index a92fefe7..253bb14e 100644 --- a/packages/column-views/stories/facets/pbdb.stories.ts +++ b/packages/column-views/stories/facets/pbdb.stories.ts @@ -3,6 +3,7 @@ import { MacrostratDataProvider, MergeSectionsMode, PBDBFossilsColumn, + FossilDataType, } from "../../src"; import h from "@macrostrat/hyper"; import { StandaloneColumn } from "../column-ui"; @@ -10,7 +11,7 @@ import { Meta } from "@storybook/react-vite"; import { ColumnAxisType } from "@macrostrat/column-components"; function PBDBFossilsDemoColumn(props) { - const { id, children, spectraColor, ...rest } = props; + const { id, children, type = FossilDataType.Collections, ...rest } = props; return h( MacrostratDataProvider, @@ -23,7 +24,7 @@ function PBDBFossilsDemoColumn(props) { allowUnitSelection: false, ...rest, }, - h(PBDBFossilsColumn, { columnID: id, color: spectraColor }), + h(PBDBFossilsColumn, { columnID: id, type }), ), ); } @@ -37,6 +38,10 @@ export default { options: ["age", "depth"], control: { type: "radio" }, }, + type: { + options: Object.values(FossilDataType), + control: { type: "select" }, + }, }, } as Meta; @@ -50,6 +55,20 @@ export const eODPColumn: Story = { showUnitPopover: true, collapseSmallUnconformities: true, keyboardNavigation: true, + type: FossilDataType.Collections, + }, +}; + +export const eODPColumnOccurrences: Story = { + args: { + id: 5576, + axisType: ColumnAxisType.DEPTH, + pixelScale: 20, + allowUnitSelection: true, + showUnitPopover: true, + collapseSmallUnconformities: true, + keyboardNavigation: true, + type: FossilDataType.Occurrences, }, }; @@ -80,6 +99,7 @@ export const eODPColumnAgeFramework: Story = { export const ParadoxBasin = { args: { id: 495, + type: "colls", }, }; diff --git a/packages/column-views/stories/sections.stories.ts b/packages/column-views/stories/sections.stories.ts index bcfc6bd7..cc0ee905 100644 --- a/packages/column-views/stories/sections.stories.ts +++ b/packages/column-views/stories/sections.stories.ts @@ -70,6 +70,16 @@ export const eODPColumnFilteredToHeightRange: Story = { }, }; +export const eODPColumnFixedPixelScale: Story = { + args: { + id: 5248, + inProcess: true, + maxInternalColumns: 1, + pixelScale: 20, + targetUnitHeight: null, + }, +}; + export const OrdinalPosition: Story = { args: { id: 432, From 019a34fab9e589592e738a6612f522356f958ad8 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sun, 23 Nov 2025 05:26:04 -0600 Subject: [PATCH 09/46] Refactor --- .../src/facets/base-sample-column.ts | 80 +++++++++++++++++++ .../column-views/src/facets/fossils/index.ts | 80 +------------------ packages/column-views/src/facets/index.ts | 1 + .../src/facets/measurements/index.ts | 3 +- .../stories/facets/pbdb.stories.ts | 1 + 5 files changed, 85 insertions(+), 80 deletions(-) create mode 100644 packages/column-views/src/facets/base-sample-column.ts diff --git a/packages/column-views/src/facets/base-sample-column.ts b/packages/column-views/src/facets/base-sample-column.ts new file mode 100644 index 00000000..391a2c9a --- /dev/null +++ b/packages/column-views/src/facets/base-sample-column.ts @@ -0,0 +1,80 @@ +import { + ColumnNotes, + getUnitHeightRange, + useMacrostratColumnData, +} from "@macrostrat/column-views"; +import { useCallback, useMemo } from "react"; +import h from "@macrostrat/hyper"; + +export interface BaseMeasurementsColumnProps { + data: T[]; + noteComponent?: any; + width?: number; + paddingLeft?: number; + className?: string; + getUnitID?: (d: T) => number | string; +} + +export function BaseMeasurementsColumn({ + data, + noteComponent, + width = 500, + paddingLeft = 40, + className, + getUnitID = (d) => d.unit_id, +}: BaseMeasurementsColumnProps) { + const { axisType, units } = useMacrostratColumnData(); + + const matchingUnit = useCallback( + (dz) => { + return (d) => { + return getUnitID(d) === dz.unit_id; + }; + }, + [getUnitID], + ); + + const notes: any[] = useMemo(() => { + if (data == null || units == null) return []; + let unitRefData = Array.from(data.values()) + .map((d) => { + return { + data: d, + unit: units.find(matchingUnit(d)), + }; + }) + .filter((d) => d.unit != null); + + unitRefData.sort((a, b) => { + const v1 = units.indexOf(a.unit); + const v2 = units.indexOf(b.unit); + return v1 - v2; + }); + + return unitRefData.map((d) => { + const { unit, data } = d; + const heightRange = getUnitHeightRange(unit, axisType); + + return { + top_height: heightRange[1], + height: heightRange[0], + data, + unit, + id: unit.unit_id, + }; + }); + }, [data, units, matchingUnit]); + + if (data == null || units == null) return null; + + return h( + "div", + { className }, + h(ColumnNotes, { + width, + paddingLeft, + notes, + noteComponent, + }), + ); +} diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index fccbab84..69eb6a3a 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -1,17 +1,11 @@ import { getUnitHeightRange } from "../../prepare-units"; import { useMacrostratColumnData } from "../../data-provider"; import hyper from "@macrostrat/hyper"; -import { - FossilDataType, - PBDBCollection, - PBDBOccurrence, - useFossilData, -} from "./provider"; +import { FossilDataType, PBDBCollection, PBDBOccurrence, useFossilData } from "./provider"; import { ColumnNotes } from "../../notes"; import { useMemo } from "react"; import type { IUnit } from "../../units"; import styles from "./index.module.sass"; -import { useCallback } from "react"; const h = hyper.styled(styles); @@ -154,75 +148,3 @@ export function PBDBFossilsColumn({ ); } -export interface BaseMeasurementsColumnProps { - data: T[]; - noteComponent?: any; - width?: number; - paddingLeft?: number; - className?: string; - getUnitID?: (d: T) => number | string; -} - -export function BaseMeasurementsColumn({ - data, - noteComponent, - width = 500, - paddingLeft = 40, - className, - getUnitID = (d) => d.unit_id, -}: BaseMeasurementsColumnProps) { - const { axisType, units } = useMacrostratColumnData(); - - const matchingUnit = useCallback( - (dz) => { - return (d) => { - return getUnitID(d) === dz.unit_id; - }; - }, - [getUnitID], - ); - - const notes: any[] = useMemo(() => { - if (data == null || units == null) return []; - let unitRefData = Array.from(data.values()) - .map((d) => { - return { - data: d, - unit: units.find(matchingUnit(d)), - }; - }) - .filter((d) => d.unit != null); - - unitRefData.sort((a, b) => { - const v1 = units.indexOf(a.unit); - const v2 = units.indexOf(b.unit); - return v1 - v2; - }); - - return unitRefData.map((d) => { - const { unit, data } = d; - const heightRange = getUnitHeightRange(unit, axisType); - - return { - top_height: heightRange[1], - height: heightRange[0], - data, - unit, - id: unit.unit_id, - }; - }); - }, [data, units, matchingUnit]); - - if (data == null || units == null) return null; - - return h( - "div", - { className }, - h(ColumnNotes, { - width, - paddingLeft, - notes, - noteComponent, - }), - ); -} diff --git a/packages/column-views/src/facets/index.ts b/packages/column-views/src/facets/index.ts index 2a98a7b3..152382df 100644 --- a/packages/column-views/src/facets/index.ts +++ b/packages/column-views/src/facets/index.ts @@ -2,3 +2,4 @@ export * from "./fossils"; export * from "./detrital-zircon"; export * from "./carbon-isotopes"; export * from "./measurements"; +export * from "./base-sample-column"; diff --git a/packages/column-views/src/facets/measurements/index.ts b/packages/column-views/src/facets/measurements/index.ts index 97ba63ed..3b063735 100644 --- a/packages/column-views/src/facets/measurements/index.ts +++ b/packages/column-views/src/facets/measurements/index.ts @@ -1,6 +1,7 @@ import h from "@macrostrat/hyper"; import { useAPIResult } from "@macrostrat/ui-components"; -import { BaseMeasurementsColumn, TruncatedList } from "../fossils"; +import { TruncatedList } from "../fossils"; +import { BaseMeasurementsColumn } from "@macrostrat/column-views"; function useSGPData({ col_id }) { const res = useAPIResult( diff --git a/packages/column-views/stories/facets/pbdb.stories.ts b/packages/column-views/stories/facets/pbdb.stories.ts index 253bb14e..9eb1d951 100644 --- a/packages/column-views/stories/facets/pbdb.stories.ts +++ b/packages/column-views/stories/facets/pbdb.stories.ts @@ -92,6 +92,7 @@ export const eODPColumnAgeFramework: Story = { inProcess: true, collapseSmallUnconformities: false, mergeSections: MergeSectionsMode.OVERLAPPING, + axisType: "age", }, title: "eODP Column (with age model applied)", }; From ab5cd1250a2bf7cad6ab5d106e5b08e44310fbf2 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sun, 23 Nov 2025 05:45:05 -0600 Subject: [PATCH 10/46] Streamlined base column provider --- .../src/facets/base-sample-column.ts | 22 +++--- .../column-views/src/facets/fossils/index.ts | 74 ++++--------------- .../src/facets/fossils/provider.ts | 6 +- 3 files changed, 33 insertions(+), 69 deletions(-) diff --git a/packages/column-views/src/facets/base-sample-column.ts b/packages/column-views/src/facets/base-sample-column.ts index 391a2c9a..e2087bcf 100644 --- a/packages/column-views/src/facets/base-sample-column.ts +++ b/packages/column-views/src/facets/base-sample-column.ts @@ -13,6 +13,7 @@ export interface BaseMeasurementsColumnProps { paddingLeft?: number; className?: string; getUnitID?: (d: T) => number | string; + matchingUnit?: (dz: T) => (d: any) => boolean; } export function BaseMeasurementsColumn({ @@ -22,17 +23,20 @@ export function BaseMeasurementsColumn({ paddingLeft = 40, className, getUnitID = (d) => d.unit_id, + matchingUnit, }: BaseMeasurementsColumnProps) { const { axisType, units } = useMacrostratColumnData(); - const matchingUnit = useCallback( - (dz) => { - return (d) => { - return getUnitID(d) === dz.unit_id; - }; - }, - [getUnitID], - ); + const _matchingUnit = + matchingUnit ?? + useCallback( + (dz) => { + return (d) => { + return getUnitID(d) === dz.unit_id; + }; + }, + [getUnitID], + ); const notes: any[] = useMemo(() => { if (data == null || units == null) return []; @@ -40,7 +44,7 @@ export function BaseMeasurementsColumn({ .map((d) => { return { data: d, - unit: units.find(matchingUnit(d)), + unit: units.find(_matchingUnit(d)), }; }) .filter((d) => d.unit != null); diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index 69eb6a3a..a0e67217 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -1,11 +1,17 @@ import { getUnitHeightRange } from "../../prepare-units"; import { useMacrostratColumnData } from "../../data-provider"; import hyper from "@macrostrat/hyper"; -import { FossilDataType, PBDBCollection, PBDBOccurrence, useFossilData } from "./provider"; +import { + FossilDataType, + PBDBCollection, + PBDBOccurrence, + useFossilData, +} from "./provider"; import { ColumnNotes } from "../../notes"; -import { useMemo } from "react"; +import { useCallback, useMemo } from "react"; import type { IUnit } from "../../units"; import styles from "./index.module.sass"; +import { BaseMeasurementsColumn } from "@macrostrat/column-views"; const h = hyper.styled(styles); @@ -80,7 +86,7 @@ function PBDBCollectionLink({ ); } -const matchingUnit = (dz) => (d) => d.unit_id == dz.unit_id; +const matchingUnit = (dz) => (d) => d.unit_id == dz[0].unit_id; export function PBDBFossilsColumn({ columnID, @@ -91,60 +97,10 @@ export function PBDBFossilsColumn({ }) { const data = useFossilData({ col_id: columnID, type }); - const { axisType, units } = useMacrostratColumnData(); - - const notes: any[] = useMemo(() => { - if (data == null || units == null) return []; - let unitRefData = Array.from(data.values()) - .map((d) => { - return { - data: d, - unit: units.find(matchingUnit(d[0])), - }; - }) - .filter((d) => d.unit != null); - - unitRefData.sort((a, b) => { - const v1 = units.indexOf(a.unit); - const v2 = units.indexOf(b.unit); - return v1 - v2; - }); - - return unitRefData.map((d) => { - const { unit, data } = d; - const heightRange = getUnitHeightRange(unit, axisType); - - return { - top_height: heightRange[1], - height: heightRange[0], - data, - unit, - id: unit.unit_id, - }; - }); - }, [data, units]); - - const width = 500; - const paddingLeft = 40; - - const noteComponent = useMemo(() => { - return (props) => { - return h(FossilInfo, { - ...props, - }); - }; - }, [width]); - - if (data == null || units == null) return null; - - return h( - "div.dz-spectra", - h(ColumnNotes, { - width, - paddingLeft, - notes, - noteComponent, - }), - ); + return h(BaseMeasurementsColumn, { + data, + noteComponent: FossilInfo, + className: "fossil-collections", + matchingUnit, + }); } - diff --git a/packages/column-views/src/facets/fossils/provider.ts b/packages/column-views/src/facets/fossils/provider.ts index 057f8c11..18b3ec2d 100644 --- a/packages/column-views/src/facets/fossils/provider.ts +++ b/packages/column-views/src/facets/fossils/provider.ts @@ -46,6 +46,7 @@ export interface PBDBCollection extends PBDBIdentifier { export interface PBDBOccurrence extends PBDBIdentifier { occ_id: number; + cltn_id: number; taxon_name: string; best_name: string; [key: string]: any; // Allow for additional properties @@ -93,7 +94,10 @@ async function fetchPDBDFossilData( ); } -async function fetchFossilData(colID: number, type: FossilDataType) { +async function fetchFossilData( + colID: number, + type: FossilDataType, +): Promise { const [macrostratData, pbdbData] = await Promise.all([ fetchMacrostratFossilData(colID, type), fetchPDBDFossilData(colID, type), From fd1da8d154e9a7cb6397ad752b2cf9afe535bae3 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sun, 23 Nov 2025 05:53:42 -0600 Subject: [PATCH 11/46] Standardize dz to use BaseMeasurementsColumn --- .../src/facets/detrital-zircon/index.ts | 92 ++++++------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/packages/column-views/src/facets/detrital-zircon/index.ts b/packages/column-views/src/facets/detrital-zircon/index.ts index 1fb2eb33..de9dbda3 100644 --- a/packages/column-views/src/facets/detrital-zircon/index.ts +++ b/packages/column-views/src/facets/detrital-zircon/index.ts @@ -6,11 +6,10 @@ import { import { IUnit } from "../../units/types"; import hyper from "@macrostrat/hyper"; import { useDetritalMeasurements, MeasurementInfo } from "./provider"; -import { useMacrostratUnits } from "../../data-provider"; -import { ColumnNotes } from "../../notes"; import { useMemo } from "react"; import styles from "./index.module.sass"; import classNames from "classnames"; +import { BaseMeasurementsColumn } from "@macrostrat/column-views"; const h = hyper.styled(styles); @@ -28,6 +27,35 @@ interface DetritalItemProps { color?: string; } +const matchingUnit = (dz) => (d) => d.unit_id == dz[0].unit_id; + +function DetritalColumn({ columnID, color = "magenta" }) { + const data = useDetritalMeasurements({ col_id: columnID }); + + const width = 400; + const paddingLeft = 40; + + const spectrumWidth = width - paddingLeft; + + const noteComponent = useMemo(() => { + return (props) => { + return h(DetritalGroup, { + width: spectrumWidth, + height: 40, + color, + ...props, + }); + }; + }, [width, color]); + + return h(BaseMeasurementsColumn, { + data, + noteComponent, + getUnitID: (d) => d[0].unit_id, + matchingUnit, + }); +} + function DepositionalAge({ unit }) { const { xScale, height } = usePlotArea(); @@ -41,7 +69,6 @@ function DepositionalAge({ unit }) { function DetritalGroup(props: DetritalItemProps) { const { note, width, height, color, spacing } = props; const { data, unit } = note; - const { geo_unit } = data[0]; const _color = color; @@ -70,63 +97,4 @@ function DetritalGroup(props: DetritalItemProps) { ); } -const matchingUnit = (dz) => (d) => d.unit_id == dz[0].unit_id; - -function DetritalColumn({ columnID, color = "magenta" }) { - const data = useDetritalMeasurements({ col_id: columnID }); - const units = useMacrostratUnits(); - - const notes: any[] = useMemo(() => { - if (data == null || units == null) return []; - let dzUnitData = Array.from(data.values()); - dzUnitData.sort((a, b) => { - const v1 = units.findIndex(matchingUnit(a)); - const v2 = units.findIndex(matchingUnit(b)); - return v1 - v2; - }); - - const data1 = dzUnitData.map((d) => { - const unit = units.find(matchingUnit(d)); - return { - top_height: unit?.t_age, - height: unit?.b_age, - data: d, - unit, - id: unit?.unit_id, - }; - }); - - return data1.filter((d) => d.unit != null); - }, [data, units]); - - const width = 400; - const paddingLeft = 40; - - const spectrumWidth = width - paddingLeft; - - const noteComponent = useMemo(() => { - return (props) => { - return h(DetritalGroup, { - width: spectrumWidth, - height: 40, - color, - ...props, - }); - }; - }, [width, color]); - - if (data == null || units == null) return null; - - return h( - "div.dz-spectra", - h(ColumnNotes, { - width, - paddingLeft, - notes, - noteComponent, - deltaConnectorAttachment: 20, - }), - ); -} - export { DetritalColumn, DetritalGroup }; From 95698e2a98b5d92d5237c73100e6b64832f679a0 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sun, 23 Nov 2025 06:01:30 -0600 Subject: [PATCH 12/46] Further standardize column views --- ...le.sass => base-sample-column.module.sass} | 0 .../src/facets/base-sample-column.ts | 34 +++++++++++++++- .../column-views/src/facets/fossils/index.ts | 40 +------------------ .../src/facets/measurements/index.ts | 4 +- 4 files changed, 36 insertions(+), 42 deletions(-) rename packages/column-views/src/facets/{fossils/index.module.sass => base-sample-column.module.sass} (100%) diff --git a/packages/column-views/src/facets/fossils/index.module.sass b/packages/column-views/src/facets/base-sample-column.module.sass similarity index 100% rename from packages/column-views/src/facets/fossils/index.module.sass rename to packages/column-views/src/facets/base-sample-column.module.sass diff --git a/packages/column-views/src/facets/base-sample-column.ts b/packages/column-views/src/facets/base-sample-column.ts index e2087bcf..2a875d10 100644 --- a/packages/column-views/src/facets/base-sample-column.ts +++ b/packages/column-views/src/facets/base-sample-column.ts @@ -4,7 +4,9 @@ import { useMacrostratColumnData, } from "@macrostrat/column-views"; import { useCallback, useMemo } from "react"; -import h from "@macrostrat/hyper"; +import hyper from "@macrostrat/hyper"; +import styles from "./base-sample-column.module.sass"; +const h = hyper.styled(styles); export interface BaseMeasurementsColumnProps { data: T[]; @@ -12,6 +14,7 @@ export interface BaseMeasurementsColumnProps { width?: number; paddingLeft?: number; className?: string; + // TODO: these props are confusing getUnitID?: (d: T) => number | string; matchingUnit?: (dz: T) => (d: any) => boolean; } @@ -82,3 +85,32 @@ export function BaseMeasurementsColumn({ }), ); } + +interface TruncatedListProps { + data: any[]; + className?: string; + maxItems?: number; + itemRenderer?: (props: { data: any }) => any; +} + +export function TruncatedList({ + data, + className, + maxItems = 5, + itemRenderer = (p) => h("span", p.data), +}: TruncatedListProps) { + let tooMany = null; + let d1 = data; + if (data.length > maxItems) { + const n = data.length - maxItems; + d1 = data.slice(0, maxItems); + tooMany = h("li.too-many", `and ${n} more`); + } + + return h("ul.truncated-list", { className }, [ + d1.map((d, i) => { + return h("li.element", { key: i }, h(itemRenderer, { data: d })); + }), + tooMany, + ]); +} diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index a0e67217..5148a767 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -1,19 +1,12 @@ -import { getUnitHeightRange } from "../../prepare-units"; -import { useMacrostratColumnData } from "../../data-provider"; -import hyper from "@macrostrat/hyper"; +import h from "@macrostrat/hyper"; import { FossilDataType, PBDBCollection, PBDBOccurrence, useFossilData, } from "./provider"; -import { ColumnNotes } from "../../notes"; -import { useCallback, useMemo } from "react"; import type { IUnit } from "../../units"; -import styles from "./index.module.sass"; -import { BaseMeasurementsColumn } from "@macrostrat/column-views"; - -const h = hyper.styled(styles); +import { BaseMeasurementsColumn, TruncatedList } from "../base-sample-column"; export { FossilDataType }; @@ -42,35 +35,6 @@ function FossilInfo(props: FossilItemProps) { }); } -interface TruncatedListProps { - data: any[]; - className?: string; - maxItems?: number; - itemRenderer?: (props: { data: any }) => any; -} - -export function TruncatedList({ - data, - className, - maxItems = 5, - itemRenderer = (p) => h("span", p.data), -}: TruncatedListProps) { - let tooMany = null; - let d1 = data; - if (data.length > maxItems) { - const n = data.length - maxItems; - d1 = data.slice(0, maxItems); - tooMany = h("li.too-many", `and ${n} more`); - } - - return h("ul.truncated-list", { className }, [ - d1.map((d, i) => { - return h("li.element", { key: i }, h(itemRenderer, { data: d })); - }), - tooMany, - ]); -} - function PBDBCollectionLink({ data, }: { diff --git a/packages/column-views/src/facets/measurements/index.ts b/packages/column-views/src/facets/measurements/index.ts index 3b063735..b04e0c8c 100644 --- a/packages/column-views/src/facets/measurements/index.ts +++ b/packages/column-views/src/facets/measurements/index.ts @@ -1,7 +1,6 @@ import h from "@macrostrat/hyper"; import { useAPIResult } from "@macrostrat/ui-components"; -import { TruncatedList } from "../fossils"; -import { BaseMeasurementsColumn } from "@macrostrat/column-views"; +import { BaseMeasurementsColumn, TruncatedList } from "../base-sample-column"; function useSGPData({ col_id }) { const res = useAPIResult( @@ -11,7 +10,6 @@ function useSGPData({ col_id }) { }, (d) => d, ); - return res; } From 5abc99929479c7d8f285fe61fccb293074db43ee Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Sun, 23 Nov 2025 18:37:36 -0600 Subject: [PATCH 13/46] Starting point for occurrence matrix --- packages/column-views/src/column.ts | 8 +- .../column-views/src/facets/fossils/index.ts | 167 +++++++++++++++++- .../src/facets/fossils/provider.ts | 9 +- .../stories/facets/pbdb.stories.ts | 24 +++ 4 files changed, 202 insertions(+), 6 deletions(-) diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 153a84b0..d25304c4 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -96,8 +96,12 @@ export function Column(props: ColumnProps) { /* Make pixelScale and targetUnitHeight mutually exclusive. PixelScale implies * standardization of scales in all sections */ let _targetUnitHeight = targetUnitHeight; + let _minSectionHeight = minSectionHeight; + let _minPixelScale = minPixelScale; if (pixelScale != null) { _targetUnitHeight = null; + _minSectionHeight = 0; + _minPixelScale = pixelScale; } const { sections, units, totalHeight } = usePreparedColumnUnits(rawUnits, { @@ -110,8 +114,8 @@ export function Column(props: ColumnProps) { targetUnitHeight: _targetUnitHeight, unconformityHeight, pixelScale, - minPixelScale, - minSectionHeight, + minPixelScale: _minPixelScale, + minSectionHeight: _minSectionHeight, collapseSmallUnconformities, }); diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index 5148a767..f4163966 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -7,6 +7,11 @@ import { } from "./provider"; import type { IUnit } from "../../units"; import { BaseMeasurementsColumn, TruncatedList } from "../base-sample-column"; +import { FlexRow, JSONView } from "@macrostrat/ui-components"; +import { InternMap } from "d3-array"; +import { ColumnAxisType, ColumnSVG } from "@macrostrat/column-components"; +import { useMacrostratColumnData } from "@macrostrat/column-views"; +import { UnitLong } from "@macrostrat/api-types"; export { FossilDataType }; @@ -59,7 +64,7 @@ export function PBDBFossilsColumn({ columnID: number; type: FossilDataType; }) { - const data = useFossilData({ col_id: columnID, type }); + const data = useFossilData(columnID, type); return h(BaseMeasurementsColumn, { data, @@ -68,3 +73,163 @@ export function PBDBFossilsColumn({ matchingUnit, }); } + +export function PBDBOccurrencesMatrix({ columnID }) { + /* A column for a matrix of taxon occurrences displayed as a table beside the main column. This will + eventually be extended with first/last occurrence markers and range bars. + */ + const data = useFossilData(columnID, FossilDataType.Occurrences) as InternMap< + number, + PBDBOccurrence[] + >; + + // convert the data to a map + const occurrenceMap = new Map(data); + + const matrix = createOccurrenceMatrix(occurrenceMap); + const col = useMacrostratColumnData(); + + const { taxonUnitMap } = matrix; + + const padding = 5; + const spacing = 8; + + const taxonEntries = Array.from(taxonUnitMap.entries()); + const taxon = taxonEntries.slice(0, 50); // limit to top 50 taxa + + return h(FlexRow, [ + h( + ColumnSVG, + { width: padding * 2 + spacing * taxon.length }, + h( + "g", + taxonEntries.map(([taxonName, unitSet], rowIndex) => { + return h(TaxonOccurrenceEntry, { + xPosition: padding + rowIndex * spacing, + units: unitSet, + }); + }), + ), + ), + h(JSONView, { data: { matrix, column: col } }), + ]); +} + +type TaxonUnitMap = Map>; + +interface OccurrenceMatrixData { + occurrenceMap: Map; // Map of unit IDs to occurrences (original data) + taxonUnitMap: TaxonUnitMap; // Map of taxon names to sets of unit IDs + taxonOccurrenceMap: Map; // Map of taxon names to occurrences +} + +function TaxonOccurrenceEntry({ + xPosition, + units, +}: { + xPosition: number; + units: Set; +}) { + const col = useMacrostratColumnData(); + const height = col.totalHeight; + return h( + "g", + { transform: `translate(${xPosition})` }, + col.sections.map((section) => { + const { units: sectionUnits, scaleInfo } = section; + + const presenceUnits = accumulatePresenceDomains( + sectionUnits, + units, + col.axisType, + ); + + console.log(presenceUnits); + + const { scale } = scaleInfo; + + return presenceUnits.map(([top, bottom]) => { + return h("line", { + y1: scale(top), + y2: scale(bottom), + stroke: "black", + strokeWidth: 2, + }); + }); + }), + ); +} + +function accumulatePresenceDomains( + unit: UnitLong[], + presenceUnits: Set, + axisType: ColumnAxisType, +): Array<[number, number]> { + const domains: Array<[number, number]> = []; + let currentDomain: [number, number] | null = null; + + for (const u of unit) { + if (presenceUnits.has(u.unit_id)) { + if (currentDomain == null) { + if ( + axisType == ColumnAxisType.DEPTH || + axisType == ColumnAxisType.HEIGHT + ) { + currentDomain = [u.t_pos, u.b_pos]; + } else { + currentDomain = [u.t_age, u.b_age]; + } + } else { + if ( + axisType == ColumnAxisType.DEPTH || + axisType == ColumnAxisType.HEIGHT + ) { + currentDomain[1] = u.b_pos; + } else { + currentDomain[1] = u.b_age; + } + } + } else { + if (currentDomain != null) { + domains.push(currentDomain); + currentDomain = null; + } + } + } + + if (currentDomain != null) { + domains.push(currentDomain); + } + + return domains; +} + +function createOccurrenceMatrix( + data: Map, +): OccurrenceMatrixData { + const taxonUnitMap = new Map>(); + const taxonOccurrenceMap = new Map(); + + for (const [unit_id, occurrences] of data.entries()) { + for (const occ of occurrences) { + const taxonName = occ.best_name ?? occ.taxon_name; + if (!taxonUnitMap.has(taxonName)) { + taxonUnitMap.set(taxonName, new Set()); + taxonOccurrenceMap.set(taxonName, []); + } + taxonUnitMap.get(taxonName).add(unit_id); + taxonOccurrenceMap.get(taxonName).push(occ); + } + } + + // sort the taxon occurrence map by number of occurrences + const sortedTaxa = Array.from(taxonUnitMap.entries()).sort((a, b) => { + return b[1].size - a[1].size; + }); + + return { + occurrenceMap: data, + taxonUnitMap: new Map(sortedTaxa), + taxonOccurrenceMap: taxonOccurrenceMap, + }; +} diff --git a/packages/column-views/src/facets/fossils/provider.ts b/packages/column-views/src/facets/fossils/provider.ts index 18b3ec2d..4b3e5b7b 100644 --- a/packages/column-views/src/facets/fossils/provider.ts +++ b/packages/column-views/src/facets/fossils/provider.ts @@ -1,4 +1,4 @@ -import { group } from "d3-array"; +import { group, InternMap } from "d3-array"; import { createAPIContext, useAPIResult, @@ -52,7 +52,10 @@ export interface PBDBOccurrence extends PBDBIdentifier { [key: string]: any; // Allow for additional properties } -export function useFossilData({ col_id, type = FossilDataType.Collections }) { +export function useFossilData( + col_id: number, + type = FossilDataType.Collections, +) { // Fossil links are stored in both Macrostrat and PBDB, depending on how the link was assembled. Here // we create a unified view of data over both sources. return useAsyncMemo(async () => { @@ -97,7 +100,7 @@ async function fetchPDBDFossilData( async function fetchFossilData( colID: number, type: FossilDataType, -): Promise { +): Promise> { const [macrostratData, pbdbData] = await Promise.all([ fetchMacrostratFossilData(colID, type), fetchPDBDFossilData(colID, type), diff --git a/packages/column-views/stories/facets/pbdb.stories.ts b/packages/column-views/stories/facets/pbdb.stories.ts index 9eb1d951..8bbe87bd 100644 --- a/packages/column-views/stories/facets/pbdb.stories.ts +++ b/packages/column-views/stories/facets/pbdb.stories.ts @@ -3,6 +3,7 @@ import { MacrostratDataProvider, MergeSectionsMode, PBDBFossilsColumn, + PBDBOccurrencesMatrix, FossilDataType, } from "../../src"; import h from "@macrostrat/hyper"; @@ -72,6 +73,29 @@ export const eODPColumnOccurrences: Story = { }, }; +export function eODPColumnWithOccurrenceMatrix() { + const id = 5576; + return h( + MacrostratDataProvider, + h( + StandaloneColumn, + { + showTimescale: false, + showLabelColumn: false, + allowUnitSelection: false, + id, + axisType: ColumnAxisType.DEPTH, + pixelScale: 20, + allowUnitSelection: true, + showUnitPopover: true, + collapseSmallUnconformities: true, + keyboardNavigation: true, + }, + h(PBDBOccurrencesMatrix, { columnID: id }), + ), + ); +} + export const eODPColumnMoreComplete: Story = { args: { id: 5278, From ca227e1e0d358b5e1048c0a5529fecfcc431c91d Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 24 Nov 2025 01:58:09 -0600 Subject: [PATCH 14/46] Control column padding --- packages/column-views/src/column.module.sass | 6 ++++ packages/column-views/src/column.ts | 34 +++++++++++++++---- .../src/facets/fossils/index.module.sass | 9 +++++ .../column-views/src/facets/fossils/index.ts | 34 +++++++++++-------- .../stories/facets/pbdb.stories.ts | 1 + 5 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 packages/column-views/src/facets/fossils/index.module.sass diff --git a/packages/column-views/src/column.module.sass b/packages/column-views/src/column.module.sass index 4fb8e778..70d4ed3a 100644 --- a/packages/column-views/src/column.module.sass +++ b/packages/column-views/src/column.module.sass @@ -33,6 +33,12 @@ body:global(.dark-mode) .column-container svg pattern image filter: invert(100%) +.column-container + padding-top: var(--column-padding-top) + padding-bottom: var(--column-padding-bottom) + padding-left: var(--column-padding-left) + padding-right: var(--column-padding-right) + .column-container :global .column display: flex diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index d25304c4..57cda09d 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -1,8 +1,14 @@ import { ColumnAxisType } from "@macrostrat/column-components"; import { hyperStyled } from "@macrostrat/hyper"; -import { useDarkMode } from "@macrostrat/ui-components"; +import { Box, extractPadding, useDarkMode } from "@macrostrat/ui-components"; import classNames from "classnames"; -import { RefObject, useRef, HTMLAttributes, useCallback } from "react"; +import { + RefObject, + useRef, + HTMLAttributes, + useCallback, + CSSProperties, +} from "react"; import styles from "./column.module.sass"; import { UnitSelectionProvider, @@ -169,6 +175,16 @@ interface ColumnInnerProps extends BaseColumnProps { } function ColumnInner(props: ColumnInnerProps) { + const padding = extractPadding(props); + + // TODO: integrate padding vars more closely with the rest of the spacing (right now padding is a bit ad-hoc) + const paddingVars = { + "--column-padding-top": `${padding.paddingTop}px`, + "--column-padding-bottom": `${padding.paddingBottom}px`, + "--column-padding-left": `${padding.paddingLeft}px`, + "--column-padding-right": `${padding.paddingRight}px`, + }; + const { unitComponent = UnitComponent, unconformityLabels = true, @@ -208,6 +224,7 @@ function ColumnInner(props: ColumnInnerProps) { ColumnContainer, { ...useMouseEventHandlers(onMouseOver), + style: paddingVars, className, }, h("div.column", { ref: columnRef }, [ @@ -279,16 +296,21 @@ function useMouseEventHandlers( export interface ColumnContainerProps extends HTMLAttributes { className?: string; + style?: CSSProperties; } export function ColumnContainer(props: ColumnContainerProps) { const { className, ...rest } = props; const darkMode = useDarkMode(); - return h("div.column-container", { - className: classNames(className, { - "dark-mode": darkMode?.isEnabled ?? false, - }), + return h(Box, { + className: classNames( + className, + { + "dark-mode": darkMode?.isEnabled ?? false, + }, + "column-container", + ), ...rest, }); } diff --git a/packages/column-views/src/facets/fossils/index.module.sass b/packages/column-views/src/facets/fossils/index.module.sass new file mode 100644 index 00000000..226223ed --- /dev/null +++ b/packages/column-views/src/facets/fossils/index.module.sass @@ -0,0 +1,9 @@ +.taxa-occurrences-matrix + color: var(--column-text-color) + line + stroke: var(--column-stroke-color) + stroke-width: 2px + text.taxon-name + fill: var(--column-text-color) + transform: rotate(90deg) translate(0, 4px) + text-anchor: end diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index f4163966..5067e41b 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -1,4 +1,4 @@ -import h from "@macrostrat/hyper"; +import hyper from "@macrostrat/hyper"; import { FossilDataType, PBDBCollection, @@ -12,6 +12,9 @@ import { InternMap } from "d3-array"; import { ColumnAxisType, ColumnSVG } from "@macrostrat/column-components"; import { useMacrostratColumnData } from "@macrostrat/column-views"; import { UnitLong } from "@macrostrat/api-types"; +import styles from "./index.module.sass"; + +const h = hyper.styled(styles); export { FossilDataType }; @@ -91,27 +94,31 @@ export function PBDBOccurrencesMatrix({ columnID }) { const { taxonUnitMap } = matrix; - const padding = 5; - const spacing = 8; + const padding = 16; + const spacing = 16; const taxonEntries = Array.from(taxonUnitMap.entries()); - const taxon = taxonEntries.slice(0, 50); // limit to top 50 taxa + //const taxon = taxonEntries.slice(0, 50); // limit to top 50 taxa return h(FlexRow, [ h( ColumnSVG, - { width: padding * 2 + spacing * taxon.length }, + { + width: padding * 2 + spacing * taxonEntries.length, + paddingTop: 200, + marginTop: -200, + }, h( - "g", + "g.taxa-occurrences-matrix", taxonEntries.map(([taxonName, unitSet], rowIndex) => { return h(TaxonOccurrenceEntry, { xPosition: padding + rowIndex * spacing, units: unitSet, + name: taxonName, }); }), ), ), - h(JSONView, { data: { matrix, column: col } }), ]); } @@ -126,15 +133,15 @@ interface OccurrenceMatrixData { function TaxonOccurrenceEntry({ xPosition, units, + name, }: { xPosition: number; units: Set; }) { const col = useMacrostratColumnData(); const height = col.totalHeight; - return h( - "g", - { transform: `translate(${xPosition})` }, + return h("g", { transform: `translate(${xPosition})` }, [ + h("g.occurrence-title", [h("text.taxon-name", name)]), col.sections.map((section) => { const { units: sectionUnits, scaleInfo } = section; @@ -152,12 +159,10 @@ function TaxonOccurrenceEntry({ return h("line", { y1: scale(top), y2: scale(bottom), - stroke: "black", - strokeWidth: 2, }); }); }), - ); + ]); } function accumulatePresenceDomains( @@ -224,7 +229,8 @@ function createOccurrenceMatrix( // sort the taxon occurrence map by number of occurrences const sortedTaxa = Array.from(taxonUnitMap.entries()).sort((a, b) => { - return b[1].size - a[1].size; + // Sort alphabetically by taxon name + return b[0].localeCompare(a[0]); }); return { diff --git a/packages/column-views/stories/facets/pbdb.stories.ts b/packages/column-views/stories/facets/pbdb.stories.ts index 8bbe87bd..ed057443 100644 --- a/packages/column-views/stories/facets/pbdb.stories.ts +++ b/packages/column-views/stories/facets/pbdb.stories.ts @@ -86,6 +86,7 @@ export function eODPColumnWithOccurrenceMatrix() { id, axisType: ColumnAxisType.DEPTH, pixelScale: 20, + paddingTop: 200, allowUnitSelection: true, showUnitPopover: true, collapseSmallUnconformities: true, From 4ec46927291e7a63eb5c90ccd534b926747ff587 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 24 Nov 2025 03:38:59 -0600 Subject: [PATCH 15/46] Fixed taxon ranges --- packages/column-views/src/column.ts | 12 +- .../src/facets/fossils/index.module.sass | 22 +++ .../column-views/src/facets/fossils/index.ts | 162 +++++++++++------- .../stories/facets/pbdb.stories.ts | 2 +- 4 files changed, 129 insertions(+), 69 deletions(-) diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 57cda09d..313b77ab 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -1,6 +1,11 @@ import { ColumnAxisType } from "@macrostrat/column-components"; import { hyperStyled } from "@macrostrat/hyper"; -import { Box, extractPadding, useDarkMode } from "@macrostrat/ui-components"; +import { + Box, + extractPadding, + Padding, + useDarkMode, +} from "@macrostrat/ui-components"; import classNames from "classnames"; import { RefObject, @@ -57,7 +62,10 @@ interface BaseColumnProps extends SectionSharedProps { ) => void; } -export interface ColumnProps extends BaseColumnProps, ColumnHeightScaleOptions { +export interface ColumnProps + extends Padding, + BaseColumnProps, + ColumnHeightScaleOptions { // Macrostrat units units: UnitLong[]; t_age?: number; diff --git a/packages/column-views/src/facets/fossils/index.module.sass b/packages/column-views/src/facets/fossils/index.module.sass index 226223ed..a29a3e5d 100644 --- a/packages/column-views/src/facets/fossils/index.module.sass +++ b/packages/column-views/src/facets/fossils/index.module.sass @@ -7,3 +7,25 @@ fill: var(--column-text-color) transform: rotate(90deg) translate(0, 4px) text-anchor: end + position: sticky + top: 0 + +.taxon-ranges + position: relative + &>svg + position: absolute + height: 100% + +.taxon-labels + position: absolute + top: 0 + left: 0 + pointer-events: none + + .taxon-label + position: absolute + width: 200px + transform: rotate(-90deg) translate(10px, 8px) + transform-origin: bottom left + + diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index 5067e41b..d598e465 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -10,7 +10,10 @@ import { BaseMeasurementsColumn, TruncatedList } from "../base-sample-column"; import { FlexRow, JSONView } from "@macrostrat/ui-components"; import { InternMap } from "d3-array"; import { ColumnAxisType, ColumnSVG } from "@macrostrat/column-components"; -import { useMacrostratColumnData } from "@macrostrat/column-views"; +import { + useCompositeScale, + useMacrostratColumnData, +} from "@macrostrat/column-views"; import { UnitLong } from "@macrostrat/api-types"; import styles from "./index.module.sass"; @@ -89,82 +92,140 @@ export function PBDBOccurrencesMatrix({ columnID }) { // convert the data to a map const occurrenceMap = new Map(data); - const matrix = createOccurrenceMatrix(occurrenceMap); const col = useMacrostratColumnData(); + const matrix = createOccurrenceMatrix(col.units, occurrenceMap, col.axisType); + + const scale = useCompositeScale(); - const { taxonUnitMap } = matrix; + const { taxonRanges } = matrix; const padding = 16; const spacing = 16; - const taxonEntries = Array.from(taxonUnitMap.entries()); + const taxonEntries = Array.from(taxonRanges.entries()); //const taxon = taxonEntries.slice(0, 50); // limit to top 50 taxa - return h(FlexRow, [ + return h("div.taxon-ranges", [ + h(TaxonOccurrenceLabels, { + taxonEntries, + padding, + spacing, + scale, + }), h( ColumnSVG, { width: padding * 2 + spacing * taxonEntries.length, - paddingTop: 200, - marginTop: -200, }, h( "g.taxa-occurrences-matrix", - taxonEntries.map(([taxonName, unitSet], rowIndex) => { - return h(TaxonOccurrenceEntry, { - xPosition: padding + rowIndex * spacing, - units: unitSet, - name: taxonName, - }); + taxonEntries.map(([taxonName, ranges], rowIndex) => { + const xPosition = padding + rowIndex * spacing; + return h("g", { transform: `translate(${xPosition})` }, [ + ranges.map(([top, bottom]) => { + return h("line", { + y1: scale(top), + y2: scale(bottom), + }); + }), + ]); }), ), ), ]); } +function TaxonOccurrenceLabels({ taxonEntries, padding, spacing, scale }) { + return h("div.taxon-labels", [ + taxonEntries.map(([taxonName, ranges], rowIndex) => { + const top = ranges[0]?.[0] ?? 0; + let topPx = scale(top) - 20; + if (topPx < 200) topPx = 0; + + return h( + "div.taxon-label", + { + style: { + top: `${topPx}px`, + left: `${padding + rowIndex * spacing}px`, + }, + }, + taxonName, + ); + }), + ]); +} + type TaxonUnitMap = Map>; interface OccurrenceMatrixData { occurrenceMap: Map; // Map of unit IDs to occurrences (original data) taxonUnitMap: TaxonUnitMap; // Map of taxon names to sets of unit IDs taxonOccurrenceMap: Map; // Map of taxon names to occurrences + taxonRanges: Map; // Map of taxon names to [top, bottom] pixel ranges } function TaxonOccurrenceEntry({ xPosition, - units, + ranges, + scale, name, }: { xPosition: number; units: Set; }) { - const col = useMacrostratColumnData(); - const height = col.totalHeight; return h("g", { transform: `translate(${xPosition})` }, [ - h("g.occurrence-title", [h("text.taxon-name", name)]), - col.sections.map((section) => { - const { units: sectionUnits, scaleInfo } = section; - - const presenceUnits = accumulatePresenceDomains( - sectionUnits, - units, - col.axisType, - ); - - console.log(presenceUnits); - - const { scale } = scaleInfo; - - return presenceUnits.map(([top, bottom]) => { - return h("line", { - y1: scale(top), - y2: scale(bottom), - }); + ranges.map(([top, bottom]) => { + return h("line", { + y1: scale(top), + y2: scale(bottom), }); }), ]); } +function createOccurrenceMatrix( + units: UnitLong[], + data: Map, + axisType: ColumnAxisType = ColumnAxisType.AGE, +): OccurrenceMatrixData { + const taxonUnitMap = new Map>(); + const taxonOccurrenceMap = new Map(); + + for (const [unit_id, occurrences] of data.entries()) { + for (const occ of occurrences) { + const taxonName = occ.best_name ?? occ.taxon_name; + if (!taxonUnitMap.has(taxonName)) { + taxonUnitMap.set(taxonName, new Set()); + taxonOccurrenceMap.set(taxonName, []); + } + taxonUnitMap.get(taxonName).add(unit_id); + taxonOccurrenceMap.get(taxonName).push(occ); + } + } + + // sort the taxon occurrence map by number of occurrences + const sortedTaxa = Array.from(taxonUnitMap.entries()).sort((a, b) => { + // Sort alphabetically by taxon name + return b[0].localeCompare(a[0]); + }); + + const taxonRanges = new Map(); + for (const [taxonName, unitSet] of taxonUnitMap.entries()) { + taxonRanges.set( + taxonName, + accumulatePresenceDomains(units, unitSet, axisType), + ); + } + + return { + occurrenceMap: data, + taxonUnitMap: new Map(sortedTaxa), + taxonOccurrenceMap: taxonOccurrenceMap, + taxonRanges, + }; +} + function accumulatePresenceDomains( unit: UnitLong[], presenceUnits: Set, @@ -208,34 +269,3 @@ function accumulatePresenceDomains( return domains; } - -function createOccurrenceMatrix( - data: Map, -): OccurrenceMatrixData { - const taxonUnitMap = new Map>(); - const taxonOccurrenceMap = new Map(); - - for (const [unit_id, occurrences] of data.entries()) { - for (const occ of occurrences) { - const taxonName = occ.best_name ?? occ.taxon_name; - if (!taxonUnitMap.has(taxonName)) { - taxonUnitMap.set(taxonName, new Set()); - taxonOccurrenceMap.set(taxonName, []); - } - taxonUnitMap.get(taxonName).add(unit_id); - taxonOccurrenceMap.get(taxonName).push(occ); - } - } - - // sort the taxon occurrence map by number of occurrences - const sortedTaxa = Array.from(taxonUnitMap.entries()).sort((a, b) => { - // Sort alphabetically by taxon name - return b[0].localeCompare(a[0]); - }); - - return { - occurrenceMap: data, - taxonUnitMap: new Map(sortedTaxa), - taxonOccurrenceMap: taxonOccurrenceMap, - }; -} diff --git a/packages/column-views/stories/facets/pbdb.stories.ts b/packages/column-views/stories/facets/pbdb.stories.ts index ed057443..144f9572 100644 --- a/packages/column-views/stories/facets/pbdb.stories.ts +++ b/packages/column-views/stories/facets/pbdb.stories.ts @@ -73,7 +73,7 @@ export const eODPColumnOccurrences: Story = { }, }; -export function eODPColumnWithOccurrenceMatrix() { +export function eODPColumnWithTaxonRanges() { const id = 5576; return h( MacrostratDataProvider, From 5f7e9083a2823595d226c837387f99e080647706 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Mon, 24 Nov 2025 04:52:14 -0600 Subject: [PATCH 16/46] Address label fit --- .../src/facets/fossils/index.module.sass | 19 ++++++++-- .../column-views/src/facets/fossils/index.ts | 37 +++++++++++++------ 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/packages/column-views/src/facets/fossils/index.module.sass b/packages/column-views/src/facets/fossils/index.module.sass index a29a3e5d..5c0bdfec 100644 --- a/packages/column-views/src/facets/fossils/index.module.sass +++ b/packages/column-views/src/facets/fossils/index.module.sass @@ -17,15 +17,28 @@ height: 100% .taxon-labels + z-index: 10 position: absolute - top: 0 - left: 0 + width: 100% + height: 100% pointer-events: none .taxon-label position: absolute - width: 200px + height: 100% + + .taxon-label-inner + position: sticky + top: calc(var(--label-width, 200px) - 10px) transform: rotate(-90deg) translate(10px, 8px) transform-origin: bottom left + background-color: var(--column-background-color) + .taxon-label-text + font-style: italic + text-align: left + padding: 0 8px + max-width: 300px + //position: sticky + //top: 200px diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index d598e465..27fd028d 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -7,7 +7,7 @@ import { } from "./provider"; import type { IUnit } from "../../units"; import { BaseMeasurementsColumn, TruncatedList } from "../base-sample-column"; -import { FlexRow, JSONView } from "@macrostrat/ui-components"; +import { Box, useElementSize } from "@macrostrat/ui-components"; import { InternMap } from "d3-array"; import { ColumnAxisType, ColumnSVG } from "@macrostrat/column-components"; import { @@ -16,6 +16,7 @@ import { } from "@macrostrat/column-views"; import { UnitLong } from "@macrostrat/api-types"; import styles from "./index.module.sass"; +import { useRef } from "react"; const h = hyper.styled(styles); @@ -105,7 +106,9 @@ export function PBDBOccurrencesMatrix({ columnID }) { const taxonEntries = Array.from(taxonRanges.entries()); //const taxon = taxonEntries.slice(0, 50); // limit to top 50 taxa - return h("div.taxon-ranges", [ + const width = padding * 2 + spacing * taxonEntries.length; + + return h(Box, { className: "taxon-ranges", width, height: col.totalHeight }, [ h(TaxonOccurrenceLabels, { taxonEntries, padding, @@ -142,20 +145,32 @@ function TaxonOccurrenceLabels({ taxonEntries, padding, spacing, scale }) { let topPx = scale(top) - 20; if (topPx < 200) topPx = 0; - return h( - "div.taxon-label", - { - style: { - top: `${topPx}px`, - left: `${padding + rowIndex * spacing}px`, - }, - }, + return h(TaxonLabel, { + top: topPx, + left: padding + rowIndex * spacing, taxonName, - ); + }); }), ]); } +function TaxonLabel({ top, left, taxonName }) { + const ref = useRef(); + const textSize = useElementSize(ref); + const labelWidth = textSize?.height ?? 200; + return h( + "div.taxon-label", + { + style: { + top: `${top}px`, + marginLeft: `${left}px`, + "--label-width": `${labelWidth}px`, + }, + }, + h("div.taxon-label-inner", h("div.taxon-label-text", { ref }, taxonName)), + ); +} + type TaxonUnitMap = Map>; interface OccurrenceMatrixData { From 4ffd83226b94c0cef0338dd1df555b0ae8abbb32 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 00:08:29 -0600 Subject: [PATCH 17/46] Starting point for nonlinear columns --- packages/column-components/src/axis.ts | 4 +- .../column-views/src/age-model-overlay.ts | 143 +++++++++++++++++- packages/column-views/src/column.ts | 6 +- .../src/correlation-chart/main.ts | 2 +- .../src/prepare-units/composite-scale.ts | 48 +++--- .../column-views/src/prepare-units/index.ts | 40 ++++- packages/column-views/src/section.ts | 5 +- .../stories/column-navigation.stories.ts | 8 + .../stories/nonlinear-scale.stories.ts | 55 +++++++ packages/timescale/src/index.ts | 2 + 10 files changed, 281 insertions(+), 32 deletions(-) create mode 100644 packages/column-views/stories/nonlinear-scale.stories.ts diff --git a/packages/column-components/src/axis.ts b/packages/column-components/src/axis.ts index baf93b20..3867c552 100644 --- a/packages/column-components/src/axis.ts +++ b/packages/column-components/src/axis.ts @@ -2,7 +2,7 @@ import { useContext, useEffect, useRef } from "react"; import h from "./hyper"; import { select } from "d3-selection"; import { axisLeft } from "d3-axis"; -import { scaleLinear, ScaleLinear } from "d3-scale"; +import { ScaleContinuousNumeric, scaleLinear, ScaleLinear } from "d3-scale"; import { useColumn } from "./context"; interface ColumnAxisProps { @@ -21,7 +21,7 @@ interface ColumnAxisProps { } interface AgeAxisProps extends ColumnAxisProps { - scale?: ScaleLinear; + scale?: ScaleContinuousNumeric; } const __d3axisKeys = [ diff --git a/packages/column-views/src/age-model-overlay.ts b/packages/column-views/src/age-model-overlay.ts index a8a1df57..48fcc246 100644 --- a/packages/column-views/src/age-model-overlay.ts +++ b/packages/column-views/src/age-model-overlay.ts @@ -25,6 +25,9 @@ import hyper from "@macrostrat/hyper"; import styles from "./age-model-overlay.module.sass"; import { useAPIResult } from "@macrostrat/ui-components"; import { useCompositeScale, useMacrostratUnits } from "./data-provider"; +import { ExtUnit } from "./prepare-units/helpers"; +import { PackageScaleInfo } from "./prepare-units/composite-scale"; +import { scaleLinear } from "d3-scale"; const h = hyper.styled(styles); interface AgeModelSurface { @@ -50,7 +53,7 @@ export function BoundaryAgeModelOverlay() { const scale = useCompositeScale(); const ageModel = useAPIResult( - "https://macrostrat.org/api/v2/age_model", + "https://dev.macrostrat.org/api/v2/age_model", { col_id }, (res) => res.success.data, ); @@ -71,3 +74,141 @@ export function BoundaryAgeModelOverlay() { }), ); } + +interface BaseSurface { + index: number; + age: number; + units_below: number[]; + units_above: number[]; +} + +function buildColumnSurfaces( + units: ExtUnit[], + tolerance: number = 0.001, +): BaseSurface[] { + /** Compute age surfaces for a column based on unit tops and bottoms */ + const surfaces: Omit[] = []; + for (const unit of units) { + // Top surface + surfaces.push({ + age: unit.t_age, + units_below: [unit.unit_id], + units_above: [], + }); + // Bottom surface + surfaces.push({ + age: unit.b_age, + units_above: [unit.unit_id], + units_below: [], + }); + } + + // Merge duplicate surfaces (same age) + const mergedSurfaces: Omit[] = []; + for (const surface of surfaces) { + const existingSurface = mergedSurfaces.find( + (s) => Math.abs(s.age - surface.age) < tolerance, + ); + if (existingSurface) { + existingSurface.units_above.push(...surface.units_above); + existingSurface.units_below.push(...surface.units_below); + } else { + mergedSurfaces.push(surface); + } + } + + // Sort surfaces by age (ascending) + mergedSurfaces.sort((a, b) => b.age - a.age); + + return mergedSurfaces.map((s, i) => ({ ...s, index: i })); +} + +interface AgeDomainUnitInfo { + t_age: number; + b_age: number; + units: ExtUnit[]; +} + +function getUnitsInAgeDomains( + surfaces: BaseSurface[], + units: ExtUnit[], +): AgeDomainUnitInfo[] { + // Get unit IDs represented between the same surface, and the proportion of their total height represented + const domainUnitInfo: AgeDomainUnitInfo[] = []; + for (let i = 0; i < surfaces.length - 1; i++) { + const topSurface = surfaces[i]; + const bottomSurface = surfaces[i + 1]; + const unitsInDomain = units.filter((unit) => { + return ( + unit.t_age <= topSurface.age + 0.001 && + unit.b_age >= bottomSurface.age - 0.001 + ); + }); + domainUnitInfo.push({ + t_age: topSurface.age, + b_age: bottomSurface.age, + units: unitsInDomain, + }); + } + return domainUnitInfo; +} + +function proportionOfUnitInDomain( + unit: ExtUnit, + t_age: number, + b_age: number, +): number { + // Compute the proportion of a unit's height that lies within the given age domain + const unitHeight = unit.t_age - unit.b_age; + if (unitHeight <= 0) return 0; + const overlapTop = Math.min(unit.t_age, t_age); + const overlapBottom = Math.max(unit.b_age, b_age); + const overlapHeight = Math.max(0, overlapTop - overlapBottom); + return overlapHeight / unitHeight; +} + +interface VariableAgeScaleOptions { + tolerance: number; + domainHeight: number; +} + +function buildVariableAgeScale( + units: ExtUnit[], + opts: VariableAgeScaleOptions, +): PackageScaleInfo[] { + /** Build a variable age scale that places age surfaces equally far apart in height space */ + const { tolerance = 0.001, domainHeight = 10 } = opts; + const surfaces = buildColumnSurfaces(units, tolerance); + const domainUnitInfo = getUnitsInAgeDomains(surfaces, units); + + const scaleInfo: PackageScaleInfo[] = []; + for (let i = 0; i < domainUnitInfo.length; i++) { + const domain = domainUnitInfo[i]; + scaleInfo.push({ + domain: [domain.b_age, domain.t_age], + pixelHeight: 1, + scale: scaleLinear(), + }); + } + return scaleInfo; +} + +export function ComputedSurfacesOverlay() { + /** Overlay showing age surfaces. This is like the boundary age model overlay but + * it is computed on the fly from unit tops and bottoms. + */ + const units = useMacrostratUnits(); + const surfaces = buildColumnSurfaces(units); + const scale = useCompositeScale(); + + return h( + "div.boundary-age-model", + surfaces.map((surface) => { + const height = scale(surface.age); + return h("div.boundary-age-model-surface", { + key: surface.index, + style: { top: `${height}px` }, + }); + }), + ); +} diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 313b77ab..9ced4930 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -43,6 +43,7 @@ import { MergeSectionsMode, usePreparedColumnUnits } from "./prepare-units"; import { UnitLong } from "@macrostrat/api-types"; import { NonIdealState } from "@blueprintjs/core"; import { DataField } from "@macrostrat/data-components"; +import { ScaleContinuousNumeric } from "d3-scale"; const h = hyperStyled(styles); @@ -79,6 +80,7 @@ export interface ColumnProps onUnitSelected?: (unitID: number | null, unit: any) => void; // Unconformity height in pixels unconformityHeight?: number; + scale?: ScaleContinuousNumeric; } export function Column(props: ColumnProps) { @@ -102,6 +104,7 @@ export function Column(props: ColumnProps) { minSectionHeight = 50, collapseSmallUnconformities = true, allowUnitSelection, + scale, ...rest } = props; const ref = useRef(); @@ -131,6 +134,7 @@ export function Column(props: ColumnProps) { minPixelScale: _minPixelScale, minSectionHeight: _minSectionHeight, collapseSmallUnconformities, + scale, }); if (sections.length === 0) { @@ -186,7 +190,7 @@ function ColumnInner(props: ColumnInnerProps) { const padding = extractPadding(props); // TODO: integrate padding vars more closely with the rest of the spacing (right now padding is a bit ad-hoc) - const paddingVars = { + const paddingVars: any = { "--column-padding-top": `${padding.paddingTop}px`, "--column-padding-bottom": `${padding.paddingBottom}px`, "--column-padding-left": `${padding.paddingLeft}px`, diff --git a/packages/column-views/src/correlation-chart/main.ts b/packages/column-views/src/correlation-chart/main.ts index 27360f36..637ed105 100644 --- a/packages/column-views/src/correlation-chart/main.ts +++ b/packages/column-views/src/correlation-chart/main.ts @@ -117,7 +117,7 @@ export function CorrelationChart({ paddingH: 4, }, packages.map((pkg, i) => { - const { offset, domain, pixelScale, key } = scaleInfo.packages[i]; + const { offset, domain, scale, key } = scaleInfo.packages[i]; return h(Package, { columnData: pkg.columnData, key, diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index c8d588b6..a08da5a6 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -1,7 +1,7 @@ import type { ExtUnit, SectionInfo } from "./helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; import { ensureArray, getUnitHeightRange } from "./utils"; -import { ScaleLinear, scaleLinear } from "d3-scale"; +import { ScaleContinuousNumeric, ScaleLinear, scaleLinear } from "d3-scale"; import { UnitLong } from "@macrostrat/api-types"; export interface ColumnHeightScaleOptions { @@ -29,19 +29,16 @@ export interface SectionScaleOptions extends ColumnHeightScaleOptions { domain: [number, number]; } -export interface LinearScaleDef { - domain: [number, number]; - pixelScale: number; -} - /** Output of a section scale. For now, this assumes that the * mapping is linear, but it could be extended to support arbitrary * scale functions. */ -export interface PackageScaleInfo extends LinearScaleDef { +export interface PackageScaleInfo { + domain: [number, number]; pixelHeight: number; // TODO: add a function - scale: ScaleLinear; + scale: ScaleContinuousNumeric; + pixelScale?: number; } export type PackageScaleLayoutData = PackageScaleInfo & { @@ -76,7 +73,7 @@ export interface CompositeColumnData } export function buildCompositeScaleInfo( - inputScales: LinearScaleDef[], + inputScales: PackageScaleInfo[], unconformityHeight: number, ): CompositeScaleData { /** Finalize the heights of sections, including the heights of unconformities @@ -88,7 +85,7 @@ export function buildCompositeScaleInfo( const packages2: PackageScaleLayoutData[] = []; for (const group of inputScales) { - const { domain, pixelScale } = group; + const { domain, scale } = group; const [b_age, t_age] = domain; const key = `package-${b_age}-${t_age}`; @@ -99,7 +96,8 @@ export function buildCompositeScaleInfo( // Unconformity height above this particular section paddingTop: totalHeight - lastSectionTopHeight, }); - const pixelHeight = pixelScale * Math.abs(b_age - t_age); + + const pixelHeight = Math.abs(scale(b_age) - scale(t_age)); lastSectionTopHeight = totalHeight + pixelHeight; totalHeight = lastSectionTopHeight + unconformityHeight; } @@ -120,6 +118,7 @@ export function finalizeSectionHeights( */ const sectionScales = sections.map((d) => d.scaleInfo); + const { totalHeight, sections: packages } = buildCompositeScaleInfo( sectionScales, unconformityHeight, @@ -142,7 +141,7 @@ export function finalizeSectionHeights( }; } -interface SectionInfoWithScale +export interface SectionInfoWithScale extends SectionInfo { scaleInfo: PackageScaleInfo; } @@ -218,7 +217,7 @@ function buildSectionScale( } export function createPackageScale( - def: LinearScaleDef, + def: PackageScaleInfo, offset: number = 0, ): PackageScaleInfo { /** Build a section scale */ @@ -369,19 +368,24 @@ export function collapseUnconformitiesByPixelHeight( currentSection = nextSection; continue; } - let dAge: number; + let heights: [number, number]; + let pxHeights: [number, number]; if (opts.axisType !== ColumnAxisType.AGE) { - dAge = Math.abs(nextSection.t_pos - currentSection.b_pos); + heights = [nextSection.t_pos, currentSection.b_pos]; } else { - dAge = Math.abs(nextSection.t_age - currentSection.b_age); + heights = [nextSection.t_age, currentSection.b_age]; } - const pxHeight = - dAge * - Math.max( - currentSection.scaleInfo.pixelScale, - nextSection.scaleInfo.pixelScale, - ); + const _diff = (vals: number[]) => { + return Math.abs(vals[0] - vals[1]); + }; + + pxHeights = [ + _diff(heights.map(nextSection.scaleInfo.scale)), + _diff(heights.map(currentSection.scaleInfo.scale)), + ]; + + const pxHeight = Math.max(...pxHeights); if (pxHeight < threshold) { let t_pos: number; diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 5d304889..7986fdd7 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -18,9 +18,11 @@ import { computeSectionHeights, finalizeSectionHeights, PackageLayoutData, + SectionInfoWithScale, } from "./composite-scale"; import type { SectionInfo } from "./helpers"; import { agesOverlap, getUnitHeightRange, unitsOverlap } from "./utils"; +import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; export * from "./utils"; export { preprocessUnits }; @@ -33,6 +35,7 @@ export interface PrepareColumnOptions extends ColumnScaleOptions { b_pos?: number; mergeSections?: MergeSectionsMode; collapseSmallUnconformities?: boolean | number; + scale?: ScaleContinuousNumeric; } export enum MergeSectionsMode { @@ -72,6 +75,7 @@ export function prepareColumnUnits( axisType, unconformityHeight, collapseSmallUnconformities = false, + scale, } = options; // Start by ensuring that ages and positions are numbers @@ -148,15 +152,45 @@ export function prepareColumnUnits( // Filter out undefined sections just in case sections = sections.filter((d) => d != null); + // SCALES + /* Compute pixel scales etc. for sections * We need to do this now to determine which unconformities * are small enough to collapse. */ - let sectionsWithScales = computeSectionHeights(sections, options); + let sectionsWithScales: SectionInfoWithScale[]; + + if (scale == null) { + sectionsWithScales = computeSectionHeights(sections, options); + } else { + sectionsWithScales = sections.map((section) => { + const { t_age, b_age, t_pos, b_pos, units } = section; + let _range = null; + // if t_age and b_age are set for a group, use them to define the range... + if (options.axisType == ColumnAxisType.AGE) { + _range = [b_age, t_age]; + } else { + _range = [b_pos, t_pos]; + } + + const pixelHeight = Math.abs(scale(_range[1]) - scale(_range[0])); + + const scaleInfo = { + domain: _range, + pixelHeight, + scale, + }; + + return { + ...section, + scaleInfo, + }; + }); + } - // Collapse small unconformities in pixel height space - // TODO: this doesn't seem to work properly for non-age columns? if (collapseSmallUnconformities ?? false) { + // Collapse small unconformities in pixel height space + // TODO: this doesn't seem to work properly for non-age columns? let threshold = unconformityHeight ?? 30; if (typeof collapseSmallUnconformities == "number") { threshold = collapseSmallUnconformities; diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 42ff8ef0..93d38a59 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -229,7 +229,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { h( "div.timescales", packages.map((group) => { - const { domain, pixelHeight, paddingTop, key } = group; + const { pixelHeight, paddingTop, key, scale } = group; return h( "div.timescale-container", { style: { paddingTop, "--timescale-level-count": nCols }, key }, @@ -240,7 +240,8 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { levels: _levels, absoluteAgeScale: true, showAgeAxis: false, - ageRange: domain as [number, number], + scale, + //ageRange: domain as [number, number], }), ], ); diff --git a/packages/column-views/stories/column-navigation.stories.ts b/packages/column-views/stories/column-navigation.stories.ts index aa961bd0..17d336ae 100644 --- a/packages/column-views/stories/column-navigation.stories.ts +++ b/packages/column-views/stories/column-navigation.stories.ts @@ -5,6 +5,7 @@ import { ColumnStoryUI } from "./column-ui"; import { MinimalUnit } from "../src/units/boxes"; import { BoundaryAgeModelOverlay, + ComputedSurfacesOverlay, EnvironmentColoredUnitComponent, } from "../src"; import { useColumnSelection } from "./column-ui/utils"; @@ -127,3 +128,10 @@ withBoundaryAgeModel.args = { axisType: "age", children: h(BoundaryAgeModelOverlay), }; + +export const withComputedSurfaces = Template.bind({}); +withComputedSurfaces.args = { + columnID: 432, + axisType: "age", + children: h(ComputedSurfacesOverlay), +}; diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts new file mode 100644 index 00000000..ad7516fc --- /dev/null +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -0,0 +1,55 @@ +import hyper from "@macrostrat/hyper"; +import styles from "./column.stories.module.sass"; +import { Meta, StoryObj } from "@storybook/react-vite"; + +import { MergeSectionsMode } from "@macrostrat/column-views"; +import "@macrostrat/style-system"; + +import { StandaloneColumn, StandaloneColumnProps } from "./column-ui"; +import { MinimalUnit } from "../src/units/boxes"; +import { scaleLog, scalePow } from "d3-scale"; +import { ColumnAxisType } from "@macrostrat/column-components"; + +const h = hyper.styled(styles); + +type Story = StoryObj; + +const meta: Meta = { + title: "Column views/Nonlinear scale", + component: StandaloneColumn, + args: { + id: 432, + unconformityLabels: true, + showLabels: true, + }, + parameters: { + docs: { + story: { + inline: false, + iframeHeight: 700, + }, + }, + }, +}; + +export default meta; + +// Logarithmic age scale + +const logScale = scaleLog().base(10).domain([0.001, 4500]).range([0, 1000]); + +const powScale = scalePow().exponent(0.5).domain([0, 4500]).range([0, 1500]); + +export const Primary: Story = { + args: { + id: 432, + mergeSections: MergeSectionsMode.ALL, + axisType: ColumnAxisType.AGE, + showLabels: false, + unitComponent: MinimalUnit, + showTimescale: true, + timescaleLevels: [1, 2], + scale: powScale, + t_age: 0, + }, +}; diff --git a/packages/timescale/src/index.ts b/packages/timescale/src/index.ts index 0c67edd4..8c0650db 100644 --- a/packages/timescale/src/index.ts +++ b/packages/timescale/src/index.ts @@ -5,6 +5,7 @@ import { TimescaleBoxes, Cursor, IntervalStyleBuilder } from "./components"; import { nestTimescale } from "./preprocess"; import { AgeAxis, AgeAxisProps } from "./age-axis"; import classNames from "classnames"; +import { ScaleContinuousNumeric } from "d3-scale"; import { useMemo } from "react"; import h from "./hyper"; @@ -30,6 +31,7 @@ interface TimescaleProps { cursorPosition?: number | null; cursorComponent?: any; intervalStyle?: IntervalStyleBuilder; + scale?: ScaleContinuousNumeric; } function TimescaleContainer(props: { From 8b11e9e7fe648e8c2f096f152a1361b2a1ad3bdd Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 00:48:52 -0600 Subject: [PATCH 18/46] Nonlinear scale is starting to work sort of --- .../src/prepare-units/composite-scale.ts | 32 ++++++++++++----- .../column-views/src/prepare-units/index.ts | 35 ++----------------- 2 files changed, 27 insertions(+), 40 deletions(-) diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index a08da5a6..2b802d91 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -1,7 +1,7 @@ import type { ExtUnit, SectionInfo } from "./helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; import { ensureArray, getUnitHeightRange } from "./utils"; -import { ScaleContinuousNumeric, ScaleLinear, scaleLinear } from "d3-scale"; +import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; import { UnitLong } from "@macrostrat/api-types"; export interface ColumnHeightScaleOptions { @@ -22,6 +22,8 @@ export interface ColumnHeightScaleOptions { unconformityHeight?: number; // Whether to collapse unconformities that are less than a height threshold collapseSmallUnconformities?: boolean | number; + // A continuous scale to use instead of generating one + scale?: ScaleContinuousNumeric; } export interface SectionScaleOptions extends ColumnHeightScaleOptions { @@ -38,7 +40,7 @@ export interface PackageScaleInfo { pixelHeight: number; // TODO: add a function scale: ScaleContinuousNumeric; - pixelScale?: number; + pixelScale?: number; // if it's a linear scale, this could be defined } export type PackageScaleLayoutData = PackageScaleInfo & { @@ -188,6 +190,7 @@ function buildSectionScale( minPixelScale = 0.2, axisType, minSectionHeight, + scale, } = opts; const domain = opts.domain ?? findSectionHeightRange(data, axisType); @@ -213,7 +216,7 @@ function buildSectionScale( height = Math.max(height, _minSectionHeight); _pixelScale = height / dAge; - return createPackageScale({ domain, pixelScale: _pixelScale }, 0); + return createPackageScale({ scale, domain, pixelScale: _pixelScale }, 0); } export function createPackageScale( @@ -222,15 +225,28 @@ export function createPackageScale( ): PackageScaleInfo { /** Build a section scale */ // Domain should be oriented from bottom to top, but scale is oriented from top to bottom - const { domain, pixelScale } = def; - const pixelHeight = pixelScale * Math.abs(domain[0] - domain[1]); + const { domain, pixelScale, scale } = def; + + let _scale = scale; + let pixelHeight: number; + if (_scale == null) { + pixelHeight = pixelScale * Math.abs(domain[0] - domain[1]); + _scale = scaleLinear() + .domain([domain[1], domain[0]]) + .range([offset, pixelHeight + offset]); + } else { + pixelHeight = Math.abs(_scale(domain[0]) - _scale(domain[1])); + _scale = _scale + .copy() + .domain([domain[1], domain[0]]) + .range([offset, pixelHeight + offset]); + } + return { domain, pixelScale, pixelHeight, - scale: scaleLinear() - .domain([domain[1], domain[0]]) - .range([offset, pixelHeight + offset]), + scale: _scale, }; } diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 7986fdd7..f05cc903 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -18,11 +18,10 @@ import { computeSectionHeights, finalizeSectionHeights, PackageLayoutData, - SectionInfoWithScale, } from "./composite-scale"; import type { SectionInfo } from "./helpers"; -import { agesOverlap, getUnitHeightRange, unitsOverlap } from "./utils"; -import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; +import { agesOverlap, unitsOverlap } from "./utils"; +import { ScaleContinuousNumeric } from "d3-scale"; export * from "./utils"; export { preprocessUnits }; @@ -158,35 +157,7 @@ export function prepareColumnUnits( * We need to do this now to determine which unconformities * are small enough to collapse. */ - let sectionsWithScales: SectionInfoWithScale[]; - - if (scale == null) { - sectionsWithScales = computeSectionHeights(sections, options); - } else { - sectionsWithScales = sections.map((section) => { - const { t_age, b_age, t_pos, b_pos, units } = section; - let _range = null; - // if t_age and b_age are set for a group, use them to define the range... - if (options.axisType == ColumnAxisType.AGE) { - _range = [b_age, t_age]; - } else { - _range = [b_pos, t_pos]; - } - - const pixelHeight = Math.abs(scale(_range[1]) - scale(_range[0])); - - const scaleInfo = { - domain: _range, - pixelHeight, - scale, - }; - - return { - ...section, - scaleInfo, - }; - }); - } + let sectionsWithScales = computeSectionHeights(sections, options); if (collapseSmallUnconformities ?? false) { // Collapse small unconformities in pixel height space From dd62bb74f1c133099ad6674a432c095932238c9e Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 00:58:06 -0600 Subject: [PATCH 19/46] Updated composite scale --- .../src/prepare-units/composite-scale.ts | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 2b802d91..850eda5e 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -23,6 +23,7 @@ export interface ColumnHeightScaleOptions { // Whether to collapse unconformities that are less than a height threshold collapseSmallUnconformities?: boolean | number; // A continuous scale to use instead of generating one + // TODO: discontinuous scales are not yet supported scale?: ScaleContinuousNumeric; } @@ -197,26 +198,34 @@ function buildSectionScale( const dAge = Math.abs(domain[0] - domain[1]); let _pixelScale = opts.pixelScale; - if (_pixelScale == null) { - const avgAgeRange = findAverageUnitHeight(data, axisType); - // Get pixel height necessary to render average unit at target height - _pixelScale = Math.max(targetUnitHeight / avgAgeRange, minPixelScale); - - // OLD METHOD that cares about overall section height vs. individual unit height - // 0.2 pixel per myr is the floor scale - //const targetHeight = targetUnitHeight * data.length; - // 1 pixel per myr is the floor scale - //_pixelScale = Math.max(targetHeight / dAge, minPixelScale); - } - - let height = dAge * _pixelScale; + let pixelHeight: number; + if (scale == null) { + if (_pixelScale == null) { + const avgAgeRange = findAverageUnitHeight(data, axisType); + // Get pixel height necessary to render average unit at target height + _pixelScale = Math.max(targetUnitHeight / avgAgeRange, minPixelScale); + + // OLD METHOD that cares about overall section height vs. individual unit height + // 0.2 pixel per myr is the floor scale + //const targetHeight = targetUnitHeight * data.length; + // 1 pixel per myr is the floor scale + //_pixelScale = Math.max(targetHeight / dAge, minPixelScale); + } - // If height is less than minSectionHeight, set it to minSectionHeight - const _minSectionHeight = minSectionHeight ?? targetUnitHeight ?? 0; - height = Math.max(height, _minSectionHeight); - _pixelScale = height / dAge; + let height = dAge * _pixelScale; + // If height is less than minSectionHeight, set it to minSectionHeight + const _minSectionHeight = minSectionHeight ?? targetUnitHeight ?? 0; + pixelHeight = Math.max(height, _minSectionHeight); + _pixelScale = pixelHeight / dAge; + } else { + // If a scale is provided, use it to compute pixel height + pixelHeight = Math.abs(scale(domain[0]) - scale(domain[1])); + } - return createPackageScale({ scale, domain, pixelScale: _pixelScale }, 0); + return createPackageScale( + { scale, domain, pixelHeight, pixelScale: _pixelScale }, + 0, + ); } export function createPackageScale( @@ -225,17 +234,18 @@ export function createPackageScale( ): PackageScaleInfo { /** Build a section scale */ // Domain should be oriented from bottom to top, but scale is oriented from top to bottom - const { domain, pixelScale, scale } = def; + const { domain, pixelScale, pixelHeight, scale } = def; + + if (scale == null && pixelScale == null) { + throw new Error("Either scale or pixelScale must be provided"); + } let _scale = scale; - let pixelHeight: number; if (_scale == null) { - pixelHeight = pixelScale * Math.abs(domain[0] - domain[1]); _scale = scaleLinear() .domain([domain[1], domain[0]]) .range([offset, pixelHeight + offset]); } else { - pixelHeight = Math.abs(_scale(domain[0]) - _scale(domain[1])); _scale = _scale .copy() .domain([domain[1], domain[0]]) From 0b77d0b8f37c8217939c746c3342522502c38605 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 01:25:34 -0600 Subject: [PATCH 20/46] Updated nonlinear scale creation --- .../column-components/src/context/column.ts | 19 ++++++++--- .../column-views/src/data-provider/base.ts | 10 ++++-- .../column-views/src/prepare-units/index.ts | 18 +++++++--- packages/column-views/src/section.ts | 3 +- .../stories/nonlinear-scale.stories.ts | 33 +++++++++++++++++-- 5 files changed, 68 insertions(+), 15 deletions(-) diff --git a/packages/column-components/src/context/column.ts b/packages/column-components/src/context/column.ts index 8ca810ab..5ca0016c 100644 --- a/packages/column-components/src/context/column.ts +++ b/packages/column-components/src/context/column.ts @@ -2,10 +2,10 @@ import { scaleLinear, ScaleContinuousNumeric, ScaleLinear } from "d3-scale"; import React, { createContext, useContext, useMemo } from "react"; import h from "@macrostrat/hyper"; -type HeightRange = [number, number]; +type HeightRange = number; type ColumnScale = ScaleContinuousNumeric | any; -type ColumnScaleClamped = ScaleLinear; +type ColumnScaleClamped = ScaleContinuousNumeric; export declare interface ColumnDivision { section_id: string; @@ -55,6 +55,7 @@ export interface ColumnProviderProps { width?: number; axisType?: ColumnAxisType; children?: any; + scale?: ColumnScale; } function ColumnProvider( @@ -75,6 +76,7 @@ function ColumnProvider( divisions = [], width = 150, axisType = ColumnAxisType.HEIGHT, + scale: _scale, ...rest } = props; @@ -103,9 +105,16 @@ function ColumnProvider( } // same as the old `innerHeight` - const pixelHeight = height * pixelsPerMeter * zoom; - - const scale = scaleLinear().domain(range).range([pixelHeight, 0]); + let scale = _scale; + let pixelHeight: number; + if (scale == null) { + pixelHeight = height * pixelsPerMeter * zoom; + scale = scaleLinear().domain(range).range([pixelHeight, 0]); + } else { + pixelHeight = Math.abs(scale.range()[1] - scale.range()[0]); + // Remove any offset that might exist from paddings, scale breaks, etc. + scale = _scale.copy().range([0, pixelHeight]); + } const scaleClamped = scale.copy().clamp(true); return { diff --git a/packages/column-views/src/data-provider/base.ts b/packages/column-views/src/data-provider/base.ts index c9c222af..d57da71e 100644 --- a/packages/column-views/src/data-provider/base.ts +++ b/packages/column-views/src/data-provider/base.ts @@ -394,10 +394,16 @@ export function MacrostratColumnProvider(props) { */ const { axisType } = useMacrostratColumnData(); - const { units, domain, pixelScale, children } = props; + const { units, domain, pixelScale, scale, children } = props; return h( ColumnProvider, - { axisType, divisions: units, range: domain, pixelsPerMeter: pixelScale }, + { + axisType, + divisions: units, + range: domain, + pixelsPerMeter: pixelScale, + scale, + }, children, ); } diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index f05cc903..539754c0 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -65,11 +65,9 @@ export function prepareColumnUnits( ): PreparedColumnData { /** Prepare units for rendering into Macrostrat columns */ + let { t_age, b_age, t_pos, b_pos } = options; + const { - t_age, - b_age, - t_pos, - b_pos, mergeSections = MergeSectionsMode.OVERLAPPING, axisType, unconformityHeight, @@ -77,6 +75,18 @@ export function prepareColumnUnits( scale, } = options; + if (scale != null) { + // Set t_age and b_age based on scale domain if not already set + const domain = scale.domain(); + if (axisType == ColumnAxisType.AGE) { + if (t_age == null) t_age = Math.min(...domain); + if (b_age == null) b_age = Math.max(...domain); + } else { + if (t_pos == null) t_pos = Math.min(...domain); + if (b_pos == null) b_pos = Math.max(...domain); + } + } + // Start by ensuring that ages and positions are numbers // also set up some values for eODP-style columns let units1 = units.map(preprocessSectionUnit); diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 93d38a59..46e72baf 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -147,7 +147,7 @@ function SectionUnits(props: SectionProps) { maxInternalColumns, } = props; - const { domain, pixelScale, pixelHeight } = scaleInfo; + const { domain, pixelScale, pixelHeight, scale } = scaleInfo; /** Ensure that we can arrange units into the maximum number * of columns defined by unitComponentProps, but that we don't @@ -179,6 +179,7 @@ function SectionUnits(props: SectionProps) { units, domain, pixelScale, // Actually pixels per myr, + scale, }, h(CompositeUnitsColumn, { width, diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index ad7516fc..c7f18736 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -7,7 +7,7 @@ import "@macrostrat/style-system"; import { StandaloneColumn, StandaloneColumnProps } from "./column-ui"; import { MinimalUnit } from "../src/units/boxes"; -import { scaleLog, scalePow } from "d3-scale"; +import { scaleLinear, scaleLog, scalePow } from "d3-scale"; import { ColumnAxisType } from "@macrostrat/column-components"; const h = hyper.styled(styles); @@ -34,13 +34,40 @@ const meta: Meta = { export default meta; +export const LinearScale: Story = { + args: { + id: 432, + mergeSections: MergeSectionsMode.ALL, + axisType: ColumnAxisType.AGE, + showLabels: false, + unitComponent: MinimalUnit, + showTimescale: true, + timescaleLevels: [1, 2], + scale: scaleLinear().domain([0, 4500]).range([0, 1500]), + }, +}; + // Logarithmic age scale const logScale = scaleLog().base(10).domain([0.001, 4500]).range([0, 1000]); -const powScale = scalePow().exponent(0.5).domain([0, 4500]).range([0, 1500]); +export const LogScale: Story = { + args: { + id: 432, + mergeSections: MergeSectionsMode.ALL, + axisType: ColumnAxisType.AGE, + showLabels: false, + unitComponent: MinimalUnit, + showTimescale: true, + timescaleLevels: [1, 2], + scale: logScale, + t_age: 0.01, + }, +}; + +const powScale = scalePow().exponent(0.5).domain([0, 500]).range([0, 1000]); -export const Primary: Story = { +export const PowerScale: Story = { args: { id: 432, mergeSections: MergeSectionsMode.ALL, From 9dea40c951273bcbfb6dad3f468e2dcfd98c9508 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 04:07:53 -0600 Subject: [PATCH 21/46] Improved nonlinear scales for correlation chart --- .../src/correlation-chart/main.ts | 10 +++++++++- .../src/correlation-chart/prepare-data.ts | 3 ++- .../stories/correlation-chart.stories.ts | 20 ++++++++++++++++++- packages/column-views/src/section.ts | 5 +++-- .../stories/column-page.stories.ts | 2 +- .../stories/nonlinear-scale.stories.ts | 3 ++- 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/packages/column-views/src/correlation-chart/main.ts b/packages/column-views/src/correlation-chart/main.ts index 637ed105..2b5fa196 100644 --- a/packages/column-views/src/correlation-chart/main.ts +++ b/packages/column-views/src/correlation-chart/main.ts @@ -33,6 +33,7 @@ import { ExtUnit } from "../prepare-units/helpers"; import { ColumnContainer } from "../column"; import { ColumnData } from "../data-provider"; import { BaseUnit } from "@macrostrat/api-types"; +import { ScaleContinuousNumeric } from "d3-scale"; const h = hyper.styled(styles); @@ -117,7 +118,8 @@ export function CorrelationChart({ paddingH: 4, }, packages.map((pkg, i) => { - const { offset, domain, scale, key } = scaleInfo.packages[i]; + const { offset, domain, pixelScale, scale, key } = + scaleInfo.packages[i]; return h(Package, { columnData: pkg.columnData, key, @@ -126,6 +128,7 @@ export function CorrelationChart({ offset, domain, pixelScale, + scale, unitComponent, }); }), @@ -147,6 +150,7 @@ function Package({ offset, domain, pixelScale, + scale, }) { return h("g.package", { transform: `translate(0 ${offset})` }, [ // Disable the SVG overlay for now @@ -160,6 +164,7 @@ function Package({ key: i, domain, pixelScale, + scale, offsetLeft: i * (columnWidth + columnSpacing), }); }), @@ -179,6 +184,7 @@ interface ColumnProps { offsetLeft?: number; domain: [number, number]; pixelScale: number; + scale?: ScaleContinuousNumeric; } function Column(props: ColumnProps) { @@ -188,6 +194,7 @@ function Column(props: ColumnProps) { offsetLeft, domain, pixelScale, + scale, unitComponent = ColoredUnitComponent, } = props; @@ -208,6 +215,7 @@ function Column(props: ColumnProps) { // Need to tighten up types here... divisions: units as any[], range: domain, + scale, pixelsPerMeter: pixelScale, // Actually pixels per myr axisType: ColumnAxisType.AGE, }, diff --git a/packages/column-views/src/correlation-chart/prepare-data.ts b/packages/column-views/src/correlation-chart/prepare-data.ts index 4f727861..eda8142d 100644 --- a/packages/column-views/src/correlation-chart/prepare-data.ts +++ b/packages/column-views/src/correlation-chart/prepare-data.ts @@ -52,6 +52,7 @@ export function buildCorrelationChartData( const opts: PrepareColumnOptions = { axisType: ColumnAxisType.AGE, targetUnitHeight, + mergeSections, ...rest, }; @@ -102,7 +103,7 @@ interface ColumnExt { interface MultiColumnPackageData { columnData: ColumnExt[]; - bestPixelScale: number; + bestPixelScale?: number; b_age: number; t_age: number; } diff --git a/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts b/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts index 60f14111..8b557e0a 100644 --- a/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts +++ b/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts @@ -6,6 +6,7 @@ import { ColumnCorrelationMap, ColumnCorrelationProvider, fetchUnits, + MergeSectionsMode, useCorrelationMapStore, } from "@macrostrat/column-views"; import { hyperStyled } from "@macrostrat/hyper"; @@ -16,6 +17,7 @@ import { ErrorBoundary, useAsyncMemo } from "@macrostrat/ui-components"; import { OverlaysProvider } from "@blueprintjs/core"; import { parseLineFromString, stringifyLine } from "../hash-string"; import { EnvironmentColoredUnitComponent } from "../../units"; +import { scaleLinear, scalePow } from "d3-scale"; const mapboxToken = import.meta.env.VITE_MAPBOX_API_TOKEN; @@ -96,7 +98,7 @@ export default { focusedLine: "-100,45 -90,50", columnSpacing: 0, columnWidth: 100, - collapseSmallUnconformities: false, + collapseSmallUnconformities: true, targetUnitHeight: 20, }, argTypes: { @@ -196,3 +198,19 @@ export const ColoredByEnvironment = Template.bind({}); ColoredByEnvironment.args = { unitComponent: EnvironmentColoredUnitComponent, }; + +export const WithFixedScale = Template.bind({}); +WithFixedScale.args = { + scale: scaleLinear().domain([0, 2500]).range([0, 1000]), +}; + +export const WithPowerScale = Template.bind({}); +WithPowerScale.args = { + scale: scalePow().exponent(0.3).domain([0, 2500]).range([0, 1000]), +}; + +export const WithPowerScaleMerged = Template.bind({}); +WithPowerScaleMerged.args = { + scale: scalePow().exponent(0.3).domain([0, 2500]).range([0, 1000]), + mergeSections: MergeSectionsMode.ALL, +}; diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 46e72baf..211b9ddd 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -252,6 +252,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { width: "100%", sections: packages, className: "unconformity-labels", + axisType: ColumnAxisType.AGE, }), ]); } @@ -259,10 +260,10 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { export function UnconformityLabels(props: { width: string | number; sections: PackageScaleLayoutData[]; + axisType?: ColumnAxisType; className?: string; }) { - const { axisType } = useMacrostratColumnData(); - const { width, sections, className } = props; + const { width, sections, className, axisType } = props; return h( "div.unconformity-labels", diff --git a/packages/column-views/stories/column-page.stories.ts b/packages/column-views/stories/column-page.stories.ts index ca3794b8..9a9a60f0 100644 --- a/packages/column-views/stories/column-page.stories.ts +++ b/packages/column-views/stories/column-page.stories.ts @@ -69,7 +69,7 @@ function ColumnStoryUI({ showUnitPopover: false, width: 450, unitComponent: ColoredUnitComponent, - collapseSmallUnconformities: false, + collapseSmallUnconformities: true, targetUnitHeight: 20, ...rest, }), diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index c7f18736..aaaa4a7d 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -43,6 +43,7 @@ export const LinearScale: Story = { unitComponent: MinimalUnit, showTimescale: true, timescaleLevels: [1, 2], + // NOTE: scale domains are clipped to the age range of the column scale: scaleLinear().domain([0, 4500]).range([0, 1500]), }, }; @@ -65,7 +66,7 @@ export const LogScale: Story = { }, }; -const powScale = scalePow().exponent(0.5).domain([0, 500]).range([0, 1000]); +const powScale = scalePow().exponent(0.5).domain([0, 4500]).range([0, 1000]); export const PowerScale: Story = { args: { From c8895c14e9afaccc843e5ccb714bec665d5ba4c8 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 11:45:59 -0600 Subject: [PATCH 22/46] Updated timescale label components and props --- .../stories/correlation-chart.stories.ts | 7 ++++ packages/timescale/src/components/index.ts | 39 ++++++++++++------- packages/timescale/src/index.ts | 24 ++++++++++-- packages/timescale/src/types.ts | 4 +- .../src/util/size-aware-label.ts | 14 +++---- 5 files changed, 62 insertions(+), 26 deletions(-) diff --git a/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts b/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts index 8b557e0a..938be8dd 100644 --- a/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts +++ b/packages/column-views/src/correlation-chart/stories/correlation-chart.stories.ts @@ -199,6 +199,13 @@ ColoredByEnvironment.args = { unitComponent: EnvironmentColoredUnitComponent, }; +export const RestrictedAgeRange = Template.bind({}); +RestrictedAgeRange.args = { + t_age: 100, + b_age: 300, + focusedLine: "-114.29,42.74 -104.59,39.21", +}; + export const WithFixedScale = Template.bind({}); WithFixedScale.args = { scale: scaleLinear().domain([0, 2500]).range([0, 1000]), diff --git a/packages/timescale/src/components/index.ts b/packages/timescale/src/components/index.ts index 7bd66f6c..7b668cd0 100644 --- a/packages/timescale/src/components/index.ts +++ b/packages/timescale/src/components/index.ts @@ -4,11 +4,6 @@ import { Interval, NestedInterval, TimescaleOrientation } from "../types"; import { useTimescale } from "../provider"; import { SizeAwareLabel } from "@macrostrat/ui-components"; -type SizeState = { - label: number; - container: number; -}; - import { CSSProperties } from "react"; export type IntervalStyleBuilder = @@ -16,13 +11,20 @@ export type IntervalStyleBuilder = | ((interval: Interval) => CSSProperties) | null; +export type LabelProps = { + shouldShow?: boolean; + allowRotation?: boolean; + positionTolerance?: number; +}; + function IntervalBox(props: { interval: Interval; - showLabel?: boolean; + labelProps?: LabelProps; intervalStyle: IntervalStyleBuilder; + allowLabelRotation?: boolean; onClick: (e: Event, interval: Interval) => void; }) { - const { interval, showLabel = true, intervalStyle, onClick } = props; + const { interval, intervalStyle, onClick, labelProps } = props; const [labelText, setLabelText] = useState(interval.nam); @@ -48,8 +50,7 @@ function IntervalBox(props: { "interval-box " + (onClick && interval.int_id != null ? "clickable" : ""), labelClassName: "interval-label", label: labelText, - tolerance: 5, - allowRotation: true, + ...(labelProps ?? {}), onVisibilityChanged(viz) { if (!viz && labelText.length > 1) { setLabelText(labelText[0]); @@ -59,12 +60,17 @@ function IntervalBox(props: { }); } -function IntervalChildren({ children, intervalStyle, onClick }) { +function IntervalChildren({ children, intervalStyle, labelProps, onClick }) { if (children == null || children.length == 0) return null; return h( "div.children", children.map((d) => { - return h(TimescaleBoxes, { interval: d, intervalStyle, onClick }); + return h(TimescaleBoxes, { + interval: d, + intervalStyle, + labelProps, + onClick, + }); }), ); } @@ -76,9 +82,10 @@ function ensureIncreasingAgeRange(ageRange) { function TimescaleBoxes(props: { interval: NestedInterval; intervalStyle: IntervalStyleBuilder; + labelProps?: LabelProps; onClick: (e: Event, interval: Interval) => void; }) { - const { interval, intervalStyle, onClick } = props; + const { interval, intervalStyle, onClick, labelProps } = props; const { scale, orientation, levels, ageRange } = useTimescale(); const { eag, lag, lvl } = interval; @@ -113,10 +120,16 @@ function TimescaleBoxes(props: { const className = slugify(name); return h("div.interval", { className, style }, [ - h.if(lvl >= minLevel)(IntervalBox, { interval, intervalStyle, onClick }), + h.if(lvl >= minLevel)(IntervalBox, { + interval, + intervalStyle, + onClick, + labelProps, + }), h.if(lvl < maxLevel)(IntervalChildren, { children, intervalStyle, + labelProps, onClick, }), ]); diff --git a/packages/timescale/src/index.ts b/packages/timescale/src/index.ts index 8c0650db..4d6f501b 100644 --- a/packages/timescale/src/index.ts +++ b/packages/timescale/src/index.ts @@ -1,12 +1,17 @@ import { defaultIntervals } from "./intervals"; import { TimescaleProvider, useTimescale } from "./provider"; import { Interval, TimescaleOrientation } from "./types"; -import { TimescaleBoxes, Cursor, IntervalStyleBuilder } from "./components"; +import { + TimescaleBoxes, + Cursor, + IntervalStyleBuilder, + LabelProps, +} from "./components"; import { nestTimescale } from "./preprocess"; import { AgeAxis, AgeAxisProps } from "./age-axis"; import classNames from "classnames"; import { ScaleContinuousNumeric } from "d3-scale"; -import { useMemo } from "react"; +import { useEffect, useMemo } from "react"; import h from "./hyper"; type ClickHandler = (event: Event, interval: any) => void; @@ -27,6 +32,7 @@ interface TimescaleProps { rootInterval?: number; /** Configuration for the axis */ axisProps?: Partial; + labelProps?: LabelProps; onClick?: ClickHandler; cursorPosition?: number | null; cursorComponent?: any; @@ -81,6 +87,7 @@ function Timescale(props: TimescaleProps) { onClick = null, intervalStyle, increaseDirection = IncreaseDirection.DOWN_LEFT, + labelProps, } = props; const [parentMap, timescale] = useMemo( @@ -108,7 +115,9 @@ function Timescale(props: TimescaleProps) { let length2 = l; - if (scale != null) { + // Warn about ambiguous usage + useEffect(() => { + if (scale == null) return; if (length != null) { console.warn( "Both scale and length provided to Timescale component. The provided scale will be used.", @@ -119,7 +128,9 @@ function Timescale(props: TimescaleProps) { "Both scale and ageRange provided to Timescale component. The provided scale will be used.", ); } + }, [scale, length, ageRange2]); + if (scale != null) { ageRange2 = scale.domain() as [number, number]; const rng = scale.range(); length2 = Math.abs(rng[1] - rng[0]); @@ -138,7 +149,12 @@ function Timescale(props: TimescaleProps) { scale, }, h(TimescaleContainer, { className }, [ - h(TimescaleBoxes, { interval: timescale, intervalStyle, onClick }), + h(TimescaleBoxes, { + interval: timescale, + intervalStyle, + onClick, + labelProps, + }), h.if(showAgeAxis)(AgeAxis, axisProps), h.if(cursorPosition != null)(cursorComponent, { age: cursorPosition }), ]), diff --git a/packages/timescale/src/types.ts b/packages/timescale/src/types.ts index b474227b..0cd69228 100644 --- a/packages/timescale/src/types.ts +++ b/packages/timescale/src/types.ts @@ -1,4 +1,4 @@ -import { ScaleLinear } from "d3-scale"; +import { ScaleContinuousNumeric, ScaleLinear } from "d3-scale"; export interface Interval { pid: number | null; @@ -36,7 +36,7 @@ interface TimescaleProviderProps { } interface TimescaleCTX extends TimescaleProviderProps { - scale: ScaleLinear | null; + scale: ScaleContinuousNumeric; } export { TimescaleCTX, NestedInterval, IntervalMap, TimescaleOrientation }; diff --git a/packages/ui-components/src/util/size-aware-label.ts b/packages/ui-components/src/util/size-aware-label.ts index b60d3ac5..8068c303 100644 --- a/packages/ui-components/src/util/size-aware-label.ts +++ b/packages/ui-components/src/util/size-aware-label.ts @@ -22,7 +22,7 @@ export type SizeAwareLabelProps = React.HTMLProps<"div"> & labelClassName: string; isShown?: boolean; onClick?: (evt: MouseEvent) => void; - tolerance?: number; + positionTolerance?: number; allowRotation?: boolean; onVisibilityChanged?( fits: boolean, @@ -48,7 +48,7 @@ function SizeAwareLabel(props: SizeAwareLabelProps) { className, labelClassName, onClick, - tolerance = 0, + positionTolerance = 0, allowRotation = false, ...rest } = props; @@ -60,8 +60,8 @@ function SizeAwareLabel(props: SizeAwareLabelProps) { const containerSz = refSize(containerRef); const labelSz = refSize(labelRef); let doesFit = - labelSz.width <= containerSz.width + 2 * tolerance && - labelSz.height <= containerSz.height + 2 * tolerance; + labelSz.width <= containerSz.width + 2 * positionTolerance && + labelSz.height <= containerSz.height + 2 * positionTolerance; if (allowRotation) { if (!doesFit) { // Try rotating the label @@ -70,8 +70,8 @@ function SizeAwareLabel(props: SizeAwareLabelProps) { height: labelSz.width, }; const rotatedFits = - rotatedLabelSz.width <= containerSz.width + 2 * tolerance && - rotatedLabelSz.height <= containerSz.height + 2 * tolerance; + rotatedLabelSz.width <= containerSz.width + 2 * positionTolerance && + rotatedLabelSz.height <= containerSz.height + 2 * positionTolerance; if (rotatedFits) { doesFit = true; setRotated(true); @@ -81,7 +81,7 @@ function SizeAwareLabel(props: SizeAwareLabelProps) { } } setFits(doesFit); - }, [containerRef, labelRef, label, tolerance, allowRotation]); + }, [containerRef, labelRef, label, positionTolerance, allowRotation]); // Report whether label fits upwards, if needed useEffect(() => { From d68e73f7ceef84827e0b761d1f495374e9eb2cac Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Tue, 25 Nov 2025 13:03:25 -0600 Subject: [PATCH 23/46] Some improvements to unconformity labels --- packages/column-views/src/column.module.sass | 32 +++++++++++++------ .../src/prepare-units/composite-scale.ts | 2 +- packages/column-views/src/section.ts | 20 ++++++++++-- .../components/unit-details/base.module.sass | 3 +- 4 files changed, 43 insertions(+), 14 deletions(-) diff --git a/packages/column-views/src/column.module.sass b/packages/column-views/src/column.module.sass index 70d4ed3a..6f5a52fe 100644 --- a/packages/column-views/src/column.module.sass +++ b/packages/column-views/src/column.module.sass @@ -117,30 +117,44 @@ body:global(.dark-mode) .column-container .unconformity - text-align: center position: absolute + font-family: var(--label-font-family, sans-serif) top: 0px - display: flex - justify-content: center - align-items: center + //font-size: 12px + font-weight: 400 + font-size: 0.8em &.giga font-weight: 800 - font-size: 1em + //font-size: 1em &.mega - font-weight: 600 font-weight: 700 - font-size: 0.9em + //font-size: 0.9em &.large - font-weight: 600 + font-weight: 500 &.small font-weight: 300 + --unit-font-weight: 400 +.unconformity-inner + //border-left: 1.5px dotted var(--secondary-color) + position: absolute + top: 5px + bottom: 5px + left: -3px + right: -2px + display: flex + justify-content: center + align-items: center + text-align: center .unconformity-text width: var(--column-width) - color: var(--column-stroke-color) --unit-font-style: normal + .prefix + font-weight: 400 + color: var(--secondary-color) + .column-title-row diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 850eda5e..f0c35be1 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -411,7 +411,7 @@ export function collapseUnconformitiesByPixelHeight( _diff(heights.map(currentSection.scaleInfo.scale)), ]; - const pxHeight = Math.max(...pxHeights); + const pxHeight = Math.min(...pxHeights); if (pxHeight < threshold) { let t_pos: number; diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 211b9ddd..0c6ea205 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -129,6 +129,7 @@ function SectionUnitsColumn(props: SectionSharedProps) { h.if(unconformityLabels)(UnconformityLabels, { width, sections: scaleData, + verbose: false, }), ]); } @@ -262,8 +263,9 @@ export function UnconformityLabels(props: { sections: PackageScaleLayoutData[]; axisType?: ColumnAxisType; className?: string; + verbose?: boolean; }) { - const { width, sections, className, axisType } = props; + const { width, sections, className, axisType, verbose } = props; return h( "div.unconformity-labels", @@ -287,12 +289,19 @@ export function UnconformityLabels(props: { height: scaleInfo.paddingTop, top, }, + verbose, }); }), ); } -function Unconformity({ upperAge, lowerAge, style, axisType }) { +function Unconformity({ + upperAge, + lowerAge, + style, + axisType, + verbose = false, +}) { if (upperAge == null || lowerAge == null) { return null; } @@ -318,7 +327,12 @@ function Unconformity({ upperAge, lowerAge, style, axisType }) { val = h(Duration, { value: ageGap }); } + let prefix: ReactNode = null; + if (verbose) { + prefix = h([" ", h("span.prefix", " gap")]); + } + return h("div.unconformity", { style, className }, [ - h("div.unconformity-text", val), + h("div.unconformity-inner", h("div.unconformity-text", [val, prefix])), ]); } diff --git a/packages/data-components/src/components/unit-details/base.module.sass b/packages/data-components/src/components/unit-details/base.module.sass index 772a3fb7..94318d50 100644 --- a/packages/data-components/src/components/unit-details/base.module.sass +++ b/packages/data-components/src/components/unit-details/base.module.sass @@ -40,8 +40,9 @@ display: block .unit - color: var(--text-subtle-color) + color: var(--unit-color, var(--text-subtle-color)) font-style: var(--text-font-style) + font-weight: var(--unit-font-weight, inherit) .parenthetical .content From 0a83b65909a71366f178b14c8b49db36d7c1a7e4 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Wed, 26 Nov 2025 03:41:40 -0600 Subject: [PATCH 24/46] Partially working dynamic scales --- .../column-views/src/age-model-overlay.ts | 122 +------ packages/column-views/src/column.ts | 2 + .../column-views/src/data-provider/store.ts | 17 +- .../src/prepare-units/composite-scale.ts | 113 ++++--- .../src/prepare-units/dynamic-scales.ts | 304 ++++++++++++++++++ .../column-views/src/prepare-units/index.ts | 46 ++- .../column-views/src/prepare-units/utils.ts | 30 +- .../stories/nonlinear-scale.stories.ts | 23 +- packages/timescale/src/index.ts | 15 - 9 files changed, 452 insertions(+), 220 deletions(-) create mode 100644 packages/column-views/src/prepare-units/dynamic-scales.ts diff --git a/packages/column-views/src/age-model-overlay.ts b/packages/column-views/src/age-model-overlay.ts index 48fcc246..de41d489 100644 --- a/packages/column-views/src/age-model-overlay.ts +++ b/packages/column-views/src/age-model-overlay.ts @@ -25,9 +25,7 @@ import hyper from "@macrostrat/hyper"; import styles from "./age-model-overlay.module.sass"; import { useAPIResult } from "@macrostrat/ui-components"; import { useCompositeScale, useMacrostratUnits } from "./data-provider"; -import { ExtUnit } from "./prepare-units/helpers"; -import { PackageScaleInfo } from "./prepare-units/composite-scale"; -import { scaleLinear } from "d3-scale"; +import { buildColumnSurfaces } from "./prepare-units/dynamic-scales"; const h = hyper.styled(styles); interface AgeModelSurface { @@ -75,124 +73,6 @@ export function BoundaryAgeModelOverlay() { ); } -interface BaseSurface { - index: number; - age: number; - units_below: number[]; - units_above: number[]; -} - -function buildColumnSurfaces( - units: ExtUnit[], - tolerance: number = 0.001, -): BaseSurface[] { - /** Compute age surfaces for a column based on unit tops and bottoms */ - const surfaces: Omit[] = []; - for (const unit of units) { - // Top surface - surfaces.push({ - age: unit.t_age, - units_below: [unit.unit_id], - units_above: [], - }); - // Bottom surface - surfaces.push({ - age: unit.b_age, - units_above: [unit.unit_id], - units_below: [], - }); - } - - // Merge duplicate surfaces (same age) - const mergedSurfaces: Omit[] = []; - for (const surface of surfaces) { - const existingSurface = mergedSurfaces.find( - (s) => Math.abs(s.age - surface.age) < tolerance, - ); - if (existingSurface) { - existingSurface.units_above.push(...surface.units_above); - existingSurface.units_below.push(...surface.units_below); - } else { - mergedSurfaces.push(surface); - } - } - - // Sort surfaces by age (ascending) - mergedSurfaces.sort((a, b) => b.age - a.age); - - return mergedSurfaces.map((s, i) => ({ ...s, index: i })); -} - -interface AgeDomainUnitInfo { - t_age: number; - b_age: number; - units: ExtUnit[]; -} - -function getUnitsInAgeDomains( - surfaces: BaseSurface[], - units: ExtUnit[], -): AgeDomainUnitInfo[] { - // Get unit IDs represented between the same surface, and the proportion of their total height represented - const domainUnitInfo: AgeDomainUnitInfo[] = []; - for (let i = 0; i < surfaces.length - 1; i++) { - const topSurface = surfaces[i]; - const bottomSurface = surfaces[i + 1]; - const unitsInDomain = units.filter((unit) => { - return ( - unit.t_age <= topSurface.age + 0.001 && - unit.b_age >= bottomSurface.age - 0.001 - ); - }); - domainUnitInfo.push({ - t_age: topSurface.age, - b_age: bottomSurface.age, - units: unitsInDomain, - }); - } - return domainUnitInfo; -} - -function proportionOfUnitInDomain( - unit: ExtUnit, - t_age: number, - b_age: number, -): number { - // Compute the proportion of a unit's height that lies within the given age domain - const unitHeight = unit.t_age - unit.b_age; - if (unitHeight <= 0) return 0; - const overlapTop = Math.min(unit.t_age, t_age); - const overlapBottom = Math.max(unit.b_age, b_age); - const overlapHeight = Math.max(0, overlapTop - overlapBottom); - return overlapHeight / unitHeight; -} - -interface VariableAgeScaleOptions { - tolerance: number; - domainHeight: number; -} - -function buildVariableAgeScale( - units: ExtUnit[], - opts: VariableAgeScaleOptions, -): PackageScaleInfo[] { - /** Build a variable age scale that places age surfaces equally far apart in height space */ - const { tolerance = 0.001, domainHeight = 10 } = opts; - const surfaces = buildColumnSurfaces(units, tolerance); - const domainUnitInfo = getUnitsInAgeDomains(surfaces, units); - - const scaleInfo: PackageScaleInfo[] = []; - for (let i = 0; i < domainUnitInfo.length; i++) { - const domain = domainUnitInfo[i]; - scaleInfo.push({ - domain: [domain.b_age, domain.t_age], - pixelHeight: 1, - scale: scaleLinear(), - }); - } - return scaleInfo; -} - export function ComputedSurfacesOverlay() { /** Overlay showing age surfaces. This is like the boundary age model overlay but * it is computed on the fly from unit tops and bottoms. diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 9ced4930..6589b7ff 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -104,6 +104,7 @@ export function Column(props: ColumnProps) { minSectionHeight = 50, collapseSmallUnconformities = true, allowUnitSelection, + hybridScaleType, scale, ...rest } = props; @@ -134,6 +135,7 @@ export function Column(props: ColumnProps) { minPixelScale: _minPixelScale, minSectionHeight: _minSectionHeight, collapseSmallUnconformities, + hybridScaleType, scale, }); diff --git a/packages/column-views/src/data-provider/store.ts b/packages/column-views/src/data-provider/store.ts index 258f7869..a4188b52 100644 --- a/packages/column-views/src/data-provider/store.ts +++ b/packages/column-views/src/data-provider/store.ts @@ -1,15 +1,9 @@ -import { - createContext, - useContext, - ReactNode, - useMemo, - useEffect, -} from "react"; +import { createContext, ReactNode, useContext, useMemo } from "react"; import h from "@macrostrat/hyper"; import { + CompositeColumnScale, createCompositeScale, PackageLayoutData, - CompositeColumnScale, } from "../prepare-units/composite-scale"; import { ExtUnit } from "../prepare-units/helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; @@ -43,11 +37,16 @@ export function MacrostratColumnDataProvider({ * */ const value = useMemo(() => { + // For now, change ordinal axis types to age axis types + let _axisType = axisType; + if (axisType == ColumnAxisType.ORDINAL) { + _axisType = ColumnAxisType.AGE; + } return { units, sections, totalHeight, - axisType, + axisType: _axisType, }; }, [units, sections, totalHeight, axisType]); diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index f0c35be1..8065ecf5 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -3,6 +3,11 @@ import { ColumnAxisType } from "@macrostrat/column-components"; import { ensureArray, getUnitHeightRange } from "./utils"; import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; import { UnitLong } from "@macrostrat/api-types"; +import { + buildColumnSurfaces, + buildScaleFromSurfaces, + HybridScaleType, +} from "./dynamic-scales"; export interface ColumnHeightScaleOptions { /** A fixed pixel scale to use for the section (pixels per Myr) */ @@ -25,6 +30,7 @@ export interface ColumnHeightScaleOptions { // A continuous scale to use instead of generating one // TODO: discontinuous scales are not yet supported scale?: ScaleContinuousNumeric; + hybridScaleType?: HybridScaleType; } export interface SectionScaleOptions extends ColumnHeightScaleOptions { @@ -192,6 +198,7 @@ function buildSectionScale( axisType, minSectionHeight, scale, + hybridScaleType, } = opts; const domain = opts.domain ?? findSectionHeightRange(data, axisType); @@ -199,6 +206,16 @@ function buildSectionScale( let _pixelScale = opts.pixelScale; let pixelHeight: number; + + if (hybridScaleType === HybridScaleType.EquidistantSurfaces) { + /** In an equidistant surfaces scale, we want to determine the heights of surfaces + * and then distribute units evenly between them. + */ + const surfaces = buildColumnSurfaces(data); + + return buildScaleFromSurfaces(surfaces, 0, _pixelScale ?? 20); + } + if (scale == null) { if (_pixelScale == null) { const avgAgeRange = findAverageUnitHeight(data, axisType); @@ -240,16 +257,18 @@ export function createPackageScale( throw new Error("Either scale or pixelScale must be provided"); } - let _scale = scale; - if (_scale == null) { + let _scale: ScaleContinuousNumeric; + if (scale == null) { _scale = scaleLinear() .domain([domain[1], domain[0]]) .range([offset, pixelHeight + offset]); } else { - _scale = _scale + const domain0 = scale.domain(); + const range0 = scale.range(); + _scale = scale .copy() - .domain([domain[1], domain[0]]) - .range([offset, pixelHeight + offset]); + .domain(domain0) + .range(range0.map((d) => d + offset)); } return { @@ -309,33 +328,49 @@ export function createCompositeScale( ): CompositeColumnScale { /** Create a scale that works across multiple packages */ // Get surfaces at which scale breaks - let scaleBreaks: [number, number][] = []; + let scaleBreaks: [number, number, any][] = []; for (const section of sections) { - const { pixelHeight, offset, domain } = section.scaleInfo; + const { pixelHeight, offset, domain, scale } = section.scaleInfo; + + console.log("Section", domain, offset); - scaleBreaks.push([domain[1], offset]); - scaleBreaks.push([domain[0], offset + pixelHeight]); + scaleBreaks.push([domain[1], offset, scale]); + scaleBreaks.push([domain[0], offset + pixelHeight, scale]); } // Sort the scale breaks by age scaleBreaks.sort((a, b) => a[0] - b[0]); + const scales: ScaleContinuousNumeric[] = []; + + let lastScale: ScaleContinuousNumeric | null = null; + for (const section of sections) { + const _scale = section.scaleInfo.scale.copy().clamp(true); + scales.push(_scale); + if (lastScale != null && interpolateUnconformities) { + // Add a new scale that interpolates between lastScale and _scale + const lastDomain = lastScale.domain(); + const lastRange = lastScale.range(); + const currentDomain = _scale.domain(); + const currentRange = _scale.range(); + + const interpScale = scaleLinear() + .domain([lastDomain[lastDomain.length - 1], currentDomain[0]]) + .range([lastRange[lastRange.length - 1], currentRange[0]]) + .clamp(true); + scales.push(interpScale); + } + lastScale = _scale; + } + const scale = (age) => { - // Accumulate scale breaks and pixel height - let lastHeight = 0; - let lastAge = null; - for (const [age1, height] of scaleBreaks) { - if (age <= age1) { - let deltaAge = age1 - lastAge; - if (deltaAge === 0) { - // If the age is exactly at a scale break, return the height at that break - return height; - } - let pixelScale = (height - lastHeight) / deltaAge; - return lastHeight + (age - lastAge) * pixelScale; + for (const s of scales) { + const domain = s.domain(); + if (age >= domain[0] && age <= domain[domain.length - 1]) { + console.log(s(age)); + return s(age); } - lastAge = age1; - lastHeight = height; } + return null; }; scale.copy = () => { @@ -344,36 +379,26 @@ export function createCompositeScale( scale.domain = () => { /** Return the domain of the scale */ - const firstSection = sections[0].scaleInfo.domain; - const lastSection = sections[sections.length - 1].scaleInfo.domain; - if (firstSection[0] < lastSection[0]) { - return [Math.min(...firstSection), Math.max(...lastSection)]; - } else { - // Catches "normal" axes like height - return [Math.max(...firstSection), Math.min(...lastSection)]; + const vals = sections.flatMap((d) => d.scaleInfo.domain); + if (vals[0] < vals[vals.length - 1]) { + // age axes + return [Math.min(...vals), Math.max(...vals)]; } + // Normal axes like height + return [Math.max(...vals), Math.min(...vals)]; }; scale.invert = (pixelHeight) => { /** Invert the scale to get the age at a given pixel height */ // Iterate through the sections to find the correct one - let lastAge = null; - for (const section of sections) { - const { - pixelHeight: sectionHeight, - pixelScale, - offset, - domain, - } = section.scaleInfo; + for (const scale of scales) { + const range = scale.range(); if ( - pixelHeight >= offset && - pixelHeight <= offset + sectionHeight && - pixelScale > 0 + pixelHeight > Math.min(...range) && + pixelHeight <= Math.max(...range) ) { - const age = domain[1] + (pixelHeight - offset) / pixelScale; - return age; + return scale.invert(pixelHeight); } - lastAge = domain[1]; } return null; }; diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts new file mode 100644 index 00000000..09579f6a --- /dev/null +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -0,0 +1,304 @@ +import { + ExtUnit, + getSectionAgeRange, + getSectionPosRange, + groupUnitsIntoSectionsByOverlap, + groupUnitsIntoSectionsBySectionID, + mergeOverlappingSections, + preprocessSectionUnit, + preprocessUnits, +} from "./helpers"; +import { ColumnAxisType } from "@macrostrat/column-components"; +import { UnitLong } from "@macrostrat/api-types"; +import { + collapseUnconformitiesByPixelHeight, + computeSectionHeights, + finalizeSectionHeights, + PackageScaleInfo, +} from "./composite-scale"; +import type { SectionInfo } from "./helpers"; +import { + agesOverlap, + MergeSectionsMode, + PrepareColumnOptions, + PreparedColumnData, + unitsOverlap, +} from "./utils"; +import { scaleLinear } from "d3-scale"; + +export enum HybridScaleType { + // An age-domain scale that puts equal vertical space between surfaces + EquidistantSurfaces = "equidistant-surfaces", + // A height-domain scale that is based on the average height of units between surfaces + ApproximateHeight = "approximate-height", +} + +export function prepareColumnUnitsEquidistant( + units: UnitLong[], + options: PrepareColumnOptions, +): PreparedColumnData { + /** Prepare units for rendering into Macrostrat columns */ + + let { t_age, b_age, t_pos, b_pos } = options; + + const { + mergeSections = MergeSectionsMode.OVERLAPPING, + unconformityHeight, + collapseSmallUnconformities = false, + scale, + } = options; + + const axisType = ColumnAxisType.AGE; + + if (scale != null) { + // Set t_age and b_age based on scale domain if not already set + const domain = scale.domain(); + if (axisType == ColumnAxisType.AGE) { + if (t_age == null) t_age = Math.min(...domain); + if (b_age == null) b_age = Math.max(...domain); + } else { + if (t_pos == null) t_pos = Math.min(...domain); + if (b_pos == null) b_pos = Math.max(...domain); + } + } + + // Start by ensuring that ages and positions are numbers + // also set up some values for eODP-style columns + let units1 = units.map(preprocessSectionUnit); + + /** Prototype filtering to age range */ + units1 = units1.filter((d) => { + // Filter units by t_age and b_age, inclusive + if (axisType == ColumnAxisType.AGE) { + return agesOverlap(d, { t_age, b_age }); + } else { + return unitsOverlap(d, { t_pos, b_pos } as any, axisType); + } + }); + + let mergeMode = mergeSections; + // if (axisType != ColumnAxisType.AGE) { + // // For non-age columns, we always merge sections. + // // This is because the "groupUnitsIntoSections" function is not well-defined + // // for non-age columns. + // mergeMode = MergeSectionsMode.ALL; + // } + + let sections0: SectionInfo[]; + if (mergeMode == MergeSectionsMode.ALL) { + // For the "merge sections" mode, we need to create a single section + const [b_unit_pos, t_unit_pos] = getSectionPosRange(units1, axisType); + const [b_unit_age, t_unit_age] = getSectionAgeRange(units1); + sections0 = [ + { + section_id: 0, + /** + * If ages limits are directly specified, use them to define the section bounds. + * */ + t_pos: t_unit_pos, + b_pos: b_unit_pos, + t_age: t_unit_age, + b_age: b_unit_age, + units: units1, + }, + ]; + } else if (axisType == ColumnAxisType.AGE) { + sections0 = groupUnitsIntoSectionsBySectionID(units1, axisType); + } else { + sections0 = groupUnitsIntoSectionsByOverlap(units1, axisType); + } + + // Limit sections to the range specified by t_age/b_age or t_pos/b_pos global options + for (let section of sections0) { + section.t_age = Math.max(section.t_age, t_age ?? -Infinity); + section.b_age = Math.min(section.b_age, b_age ?? Infinity); + } + + /** Merging overlapping sections really only makes sense for age/height/depth + * columns. Ordinal columns are numbered by section so merging them + * results in collisions. + */ + let sections = sections0; + if ( + mergeSections == MergeSectionsMode.OVERLAPPING && + axisType == ColumnAxisType.AGE + ) { + sections = mergeOverlappingSections(sections); + } + // Filter out undefined sections just in case + sections = sections.filter((d) => d != null); + + // SCALES + + /* Compute pixel scales etc. for sections + * We need to do this now to determine which unconformities + * are small enough to collapse. + */ + let sectionsWithScales = computeSectionHeights(sections, options); + + /** Prepare section scale information using groups */ + let { totalHeight, sections: sections2 } = finalizeSectionHeights( + sectionsWithScales, + unconformityHeight, + axisType, + ); + + /** For each section, find units that are overlapping. + * We do this after merging sections so that we can + * handle cases where there are overlapping units across sections + * */ + const sectionsOut = sections2.map((section) => { + return { + ...section, + units: preprocessUnits(section, axisType), + }; + }); + + /** Reconstitute the units so that they are sorted by section and properly enhanced. + * This is mostly important so that unit keyboard navigation + * predictably selects adjacent units. + */ + const units2 = sectionsOut.reduce((acc, group) => { + const { units } = group; + for (const unit of units) { + acc.push(unit); + } + return acc; + }, []); + + return { + units: units2, + totalHeight, + sections: sectionsOut, + }; +} + +interface BaseSurface { + index: number; + age: number; + units_below: number[]; + units_above: number[]; +} + +export function buildColumnSurfaces( + units: T[], + tolerance: number = 0.001, +): BaseSurface[] { + /** Compute age surfaces for a column based on unit tops and bottoms */ + const surfaces: Omit[] = []; + for (const unit of units) { + // Top surface + surfaces.push({ + age: unit.t_age, + units_below: [unit.unit_id], + units_above: [], + }); + // Bottom surface + surfaces.push({ + age: unit.b_age, + units_above: [unit.unit_id], + units_below: [], + }); + } + + // Merge duplicate surfaces (same age) + const mergedSurfaces: Omit[] = []; + for (const surface of surfaces) { + const existingSurface = mergedSurfaces.find( + (s) => Math.abs(s.age - surface.age) < tolerance, + ); + if (existingSurface) { + existingSurface.units_above.push(...surface.units_above); + existingSurface.units_below.push(...surface.units_below); + } else { + mergedSurfaces.push(surface); + } + } + + // Sort surfaces by age (ascending) + mergedSurfaces.sort((a, b) => a.age - b.age); + + return mergedSurfaces.map((s, i) => ({ ...s, index: i })); +} + +interface AgeDomainUnitInfo { + t_age: number; + b_age: number; + units: ExtUnit[]; +} + +function getUnitsInAgeDomains( + surfaces: BaseSurface[], + units: ExtUnit[], +): AgeDomainUnitInfo[] { + // Get unit IDs represented between the same surface, and the proportion of their total height represented + const domainUnitInfo: AgeDomainUnitInfo[] = []; + for (let i = 0; i < surfaces.length - 1; i++) { + const topSurface = surfaces[i]; + const bottomSurface = surfaces[i + 1]; + const unitsInDomain = units.filter((unit) => { + return ( + unit.t_age <= topSurface.age + 0.001 && + unit.b_age >= bottomSurface.age - 0.001 + ); + }); + domainUnitInfo.push({ + t_age: topSurface.age, + b_age: bottomSurface.age, + units: unitsInDomain, + }); + } + return domainUnitInfo; +} + +function proportionOfUnitInDomain( + unit: ExtUnit, + t_age: number, + b_age: number, +): number { + // Compute the proportion of a unit's height that lies within the given age domain + const unitHeight = unit.t_age - unit.b_age; + if (unitHeight <= 0) return 0; + const overlapTop = Math.min(unit.t_age, t_age); + const overlapBottom = Math.max(unit.b_age, b_age); + const overlapHeight = Math.max(0, overlapTop - overlapBottom); + return overlapHeight / unitHeight; +} + +interface VariableAgeScaleOptions { + tolerance: number; + domainHeight: number; +} + +export function buildScaleFromSurfaces( + surfaces: BaseSurface[], + pixelOffset: number, // height in pixels at which to start the scale + pixelScale: number, // pixels per unit +): PackageScaleInfo { + /** Build a variable age scale that places age surfaces equally far apart in height space. + * It is presumed that gaps are already removed from the unit set provided. + * */ + + const domain: [number, number] = [ + surfaces[surfaces.length - 1].age, + surfaces[0].age, + ]; + // Compute the height in pixels for each surface + + const surfaceHeights = surfaces.map((surface, i) => { + return i * 20; + }); + + // Build a piecewise linear scale mapping age to pixel height + const ageDomain = surfaces.map((s) => s.age); + const pixelRange = surfaceHeights; + + const scale = scaleLinear().domain(ageDomain).range(pixelRange); + + return { + scale, + pixelScale: null, // pixels per unit + domain, + pixelHeight: Math.abs(pixelRange[pixelRange.length - 1] - pixelRange[0]), + }; +} diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 539754c0..26a54a27 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -9,44 +9,24 @@ import { } from "./helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; import { useMemo } from "react"; -import type { ExtUnit } from "./helpers"; import { UnitLong } from "@macrostrat/api-types"; import { collapseUnconformitiesByPixelHeight, - ColumnScaleOptions, - CompositeColumnData, computeSectionHeights, finalizeSectionHeights, - PackageLayoutData, } from "./composite-scale"; import type { SectionInfo } from "./helpers"; -import { agesOverlap, unitsOverlap } from "./utils"; -import { ScaleContinuousNumeric } from "d3-scale"; +import { + agesOverlap, + MergeSectionsMode, + PrepareColumnOptions, + PreparedColumnData, + unitsOverlap, +} from "./utils"; export * from "./utils"; export { preprocessUnits }; -export interface PrepareColumnOptions extends ColumnScaleOptions { - axisType: ColumnAxisType; - t_age?: number; - b_age?: number; - t_pos?: number; - b_pos?: number; - mergeSections?: MergeSectionsMode; - collapseSmallUnconformities?: boolean | number; - scale?: ScaleContinuousNumeric; -} - -export enum MergeSectionsMode { - ALL = "all", - OVERLAPPING = "overlapping", -} - -export interface PreparedColumnData extends CompositeColumnData { - sections: PackageLayoutData[]; - units: ExtUnit[]; -} - export function usePreparedColumnUnits( data: UnitLong[], options: PrepareColumnOptions, @@ -72,9 +52,19 @@ export function prepareColumnUnits( axisType, unconformityHeight, collapseSmallUnconformities = false, + hybridScaleType, scale, } = options; + /* Ordinal positioning does not really make sense for columns with overlapping + units, and there is little value to using API-provided ordinal positions anyway, + as they tend to be arbitrary. Therefore, we ignore t_pos and b_pos values for these + columns and compute an ordinal positioning of _surfaces_ within sections instead. + */ + // if (hybridScaleType == HybridScaleType.EquidistantSurfaces) { + // return prepareColumnUnitsEquidistant(units, options); + // } + if (scale != null) { // Set t_age and b_age based on scale domain if not already set const domain = scale.domain(); @@ -169,7 +159,7 @@ export function prepareColumnUnits( */ let sectionsWithScales = computeSectionHeights(sections, options); - if (collapseSmallUnconformities ?? false) { + if (collapseSmallUnconformities && hybridScaleType == null) { // Collapse small unconformities in pixel height space // TODO: this doesn't seem to work properly for non-age columns? let threshold = unconformityHeight ?? 30; diff --git a/packages/column-views/src/prepare-units/utils.ts b/packages/column-views/src/prepare-units/utils.ts index 70b2c429..a9d4c99b 100644 --- a/packages/column-views/src/prepare-units/utils.ts +++ b/packages/column-views/src/prepare-units/utils.ts @@ -4,10 +4,38 @@ import { compareAgeRanges, } from "@macrostrat/stratigraphy-utils"; import { ColumnAxisType } from "@macrostrat/column-components"; -import { StratigraphicPackage } from "./helpers"; +import { type ExtUnit, StratigraphicPackage } from "./helpers"; +import { + ColumnScaleOptions, + CompositeColumnData, + PackageLayoutData, +} from "./composite-scale"; +import { ScaleContinuousNumeric } from "d3-scale"; +import { HybridScaleType } from "./dynamic-scales"; const dt = 0.001; +export interface PrepareColumnOptions extends ColumnScaleOptions { + axisType: ColumnAxisType; + t_age?: number; + b_age?: number; + t_pos?: number; + b_pos?: number; + mergeSections?: MergeSectionsMode; + collapseSmallUnconformities?: boolean | number; + scale?: ScaleContinuousNumeric; +} + +export enum MergeSectionsMode { + ALL = "all", + OVERLAPPING = "overlapping", +} + +export interface PreparedColumnData extends CompositeColumnData { + sections: PackageLayoutData[]; + units: ExtUnit[]; +} + interface UnitsOverlap { ( a: StratigraphicPackage, diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index aaaa4a7d..84f8ef37 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -2,13 +2,18 @@ import hyper from "@macrostrat/hyper"; import styles from "./column.stories.module.sass"; import { Meta, StoryObj } from "@storybook/react-vite"; -import { MergeSectionsMode } from "@macrostrat/column-views"; +import { + ColoredUnitComponent, + ComputedSurfacesOverlay, + MergeSectionsMode, +} from "@macrostrat/column-views"; import "@macrostrat/style-system"; import { StandaloneColumn, StandaloneColumnProps } from "./column-ui"; import { MinimalUnit } from "../src/units/boxes"; import { scaleLinear, scaleLog, scalePow } from "d3-scale"; import { ColumnAxisType } from "@macrostrat/column-components"; +import { HybridScaleType } from "../src/prepare-units/dynamic-scales"; const h = hyper.styled(styles); @@ -66,7 +71,7 @@ export const LogScale: Story = { }, }; -const powScale = scalePow().exponent(0.5).domain([0, 4500]).range([0, 1000]); +const powScale = scalePow().exponent(0.5).domain([0, 4500]).range([0, 600]); export const PowerScale: Story = { args: { @@ -81,3 +86,17 @@ export const PowerScale: Story = { t_age: 0, }, }; + +export const EquidistantSurfaces: Story = { + args: { + id: 432, + // Ordered time bins + axisType: ColumnAxisType.AGE, + hybridScaleType: HybridScaleType.EquidistantSurfaces, + showLabels: false, + unitComponent: ColoredUnitComponent, + showTimescale: true, + timescaleLevels: [1, 3], + children: h(ComputedSurfacesOverlay), + }, +}; diff --git a/packages/timescale/src/index.ts b/packages/timescale/src/index.ts index 4d6f501b..461ed931 100644 --- a/packages/timescale/src/index.ts +++ b/packages/timescale/src/index.ts @@ -115,21 +115,6 @@ function Timescale(props: TimescaleProps) { let length2 = l; - // Warn about ambiguous usage - useEffect(() => { - if (scale == null) return; - if (length != null) { - console.warn( - "Both scale and length provided to Timescale component. The provided scale will be used.", - ); - } - if (ageRange2 != null) { - console.warn( - "Both scale and ageRange provided to Timescale component. The provided scale will be used.", - ); - } - }, [scale, length, ageRange2]); - if (scale != null) { ageRange2 = scale.domain() as [number, number]; const rng = scale.range(); From 2ce070a6f18e72eed01b0d4e04fa89f8bc256136 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Wed, 26 Nov 2025 04:15:28 -0600 Subject: [PATCH 25/46] Update composite scale for equidistant surfaces --- .../column-components/src/context/column.ts | 3 +- .../column-components/src/notes/layout.ts | 7 +- .../column-views/src/data-provider/store.ts | 6 +- .../src/prepare-units/composite-scale.ts | 10 +- .../src/prepare-units/dynamic-scales.ts | 173 +----------------- packages/column-views/src/units/composite.ts | 52 +----- .../stories/nonlinear-scale.stories.ts | 3 +- 7 files changed, 27 insertions(+), 227 deletions(-) diff --git a/packages/column-components/src/context/column.ts b/packages/column-components/src/context/column.ts index 5ca0016c..5fd22c77 100644 --- a/packages/column-components/src/context/column.ts +++ b/packages/column-components/src/context/column.ts @@ -113,7 +113,8 @@ function ColumnProvider( } else { pixelHeight = Math.abs(scale.range()[1] - scale.range()[0]); // Remove any offset that might exist from paddings, scale breaks, etc. - scale = _scale.copy().range([0, pixelHeight]); + const r1 = scale.range().map((d) => d - scale.range()[0]); + scale = _scale.copy().range(r1); } const scaleClamped = scale.copy().clamp(true); diff --git a/packages/column-components/src/notes/layout.ts b/packages/column-components/src/notes/layout.ts index cd872038..c7ba031c 100644 --- a/packages/column-components/src/notes/layout.ts +++ b/packages/column-components/src/notes/layout.ts @@ -48,10 +48,13 @@ const buildColumnIndex = function () { function withinDomain(scale) { const scaleDomain = scale.domain(); + const d1: [number, number] = [ + Math.min(...scaleDomain), + Math.max(...scaleDomain), + ]; return (d) => { const noteRange: [number, number] = [d.height, d.top_height ?? d.height]; - - const rel = compareAgeRanges(scaleDomain, noteRange); + const rel = compareAgeRanges(d1, noteRange); return rel !== AgeRangeRelationship.Disjoint; }; diff --git a/packages/column-views/src/data-provider/store.ts b/packages/column-views/src/data-provider/store.ts index a4188b52..db298cf5 100644 --- a/packages/column-views/src/data-provider/store.ts +++ b/packages/column-views/src/data-provider/store.ts @@ -38,15 +38,11 @@ export function MacrostratColumnDataProvider({ const value = useMemo(() => { // For now, change ordinal axis types to age axis types - let _axisType = axisType; - if (axisType == ColumnAxisType.ORDINAL) { - _axisType = ColumnAxisType.AGE; - } return { units, sections, totalHeight, - axisType: _axisType, + axisType, }; }, [units, sections, totalHeight, axisType]); diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 8065ecf5..2101d053 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -366,7 +366,6 @@ export function createCompositeScale( for (const s of scales) { const domain = s.domain(); if (age >= domain[0] && age <= domain[domain.length - 1]) { - console.log(s(age)); return s(age); } } @@ -397,12 +396,21 @@ export function createCompositeScale( pixelHeight > Math.min(...range) && pixelHeight <= Math.max(...range) ) { + console.log("Inverting scale at pixel height", pixelHeight); return scale.invert(pixelHeight); } } return null; }; + scale.clamp = (clamp: boolean) => { + /** Clamp all constituent scales */ + for (const s of scales) { + s.clamp(clamp); + } + return scale; + }; + return scale as CompositeColumnScale; } diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index 09579f6a..61aa4805 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -1,29 +1,6 @@ -import { - ExtUnit, - getSectionAgeRange, - getSectionPosRange, - groupUnitsIntoSectionsByOverlap, - groupUnitsIntoSectionsBySectionID, - mergeOverlappingSections, - preprocessSectionUnit, - preprocessUnits, -} from "./helpers"; -import { ColumnAxisType } from "@macrostrat/column-components"; +import { ExtUnit } from "./helpers"; import { UnitLong } from "@macrostrat/api-types"; -import { - collapseUnconformitiesByPixelHeight, - computeSectionHeights, - finalizeSectionHeights, - PackageScaleInfo, -} from "./composite-scale"; -import type { SectionInfo } from "./helpers"; -import { - agesOverlap, - MergeSectionsMode, - PrepareColumnOptions, - PreparedColumnData, - unitsOverlap, -} from "./utils"; +import { PackageScaleInfo } from "./composite-scale"; import { scaleLinear } from "d3-scale"; export enum HybridScaleType { @@ -33,146 +10,6 @@ export enum HybridScaleType { ApproximateHeight = "approximate-height", } -export function prepareColumnUnitsEquidistant( - units: UnitLong[], - options: PrepareColumnOptions, -): PreparedColumnData { - /** Prepare units for rendering into Macrostrat columns */ - - let { t_age, b_age, t_pos, b_pos } = options; - - const { - mergeSections = MergeSectionsMode.OVERLAPPING, - unconformityHeight, - collapseSmallUnconformities = false, - scale, - } = options; - - const axisType = ColumnAxisType.AGE; - - if (scale != null) { - // Set t_age and b_age based on scale domain if not already set - const domain = scale.domain(); - if (axisType == ColumnAxisType.AGE) { - if (t_age == null) t_age = Math.min(...domain); - if (b_age == null) b_age = Math.max(...domain); - } else { - if (t_pos == null) t_pos = Math.min(...domain); - if (b_pos == null) b_pos = Math.max(...domain); - } - } - - // Start by ensuring that ages and positions are numbers - // also set up some values for eODP-style columns - let units1 = units.map(preprocessSectionUnit); - - /** Prototype filtering to age range */ - units1 = units1.filter((d) => { - // Filter units by t_age and b_age, inclusive - if (axisType == ColumnAxisType.AGE) { - return agesOverlap(d, { t_age, b_age }); - } else { - return unitsOverlap(d, { t_pos, b_pos } as any, axisType); - } - }); - - let mergeMode = mergeSections; - // if (axisType != ColumnAxisType.AGE) { - // // For non-age columns, we always merge sections. - // // This is because the "groupUnitsIntoSections" function is not well-defined - // // for non-age columns. - // mergeMode = MergeSectionsMode.ALL; - // } - - let sections0: SectionInfo[]; - if (mergeMode == MergeSectionsMode.ALL) { - // For the "merge sections" mode, we need to create a single section - const [b_unit_pos, t_unit_pos] = getSectionPosRange(units1, axisType); - const [b_unit_age, t_unit_age] = getSectionAgeRange(units1); - sections0 = [ - { - section_id: 0, - /** - * If ages limits are directly specified, use them to define the section bounds. - * */ - t_pos: t_unit_pos, - b_pos: b_unit_pos, - t_age: t_unit_age, - b_age: b_unit_age, - units: units1, - }, - ]; - } else if (axisType == ColumnAxisType.AGE) { - sections0 = groupUnitsIntoSectionsBySectionID(units1, axisType); - } else { - sections0 = groupUnitsIntoSectionsByOverlap(units1, axisType); - } - - // Limit sections to the range specified by t_age/b_age or t_pos/b_pos global options - for (let section of sections0) { - section.t_age = Math.max(section.t_age, t_age ?? -Infinity); - section.b_age = Math.min(section.b_age, b_age ?? Infinity); - } - - /** Merging overlapping sections really only makes sense for age/height/depth - * columns. Ordinal columns are numbered by section so merging them - * results in collisions. - */ - let sections = sections0; - if ( - mergeSections == MergeSectionsMode.OVERLAPPING && - axisType == ColumnAxisType.AGE - ) { - sections = mergeOverlappingSections(sections); - } - // Filter out undefined sections just in case - sections = sections.filter((d) => d != null); - - // SCALES - - /* Compute pixel scales etc. for sections - * We need to do this now to determine which unconformities - * are small enough to collapse. - */ - let sectionsWithScales = computeSectionHeights(sections, options); - - /** Prepare section scale information using groups */ - let { totalHeight, sections: sections2 } = finalizeSectionHeights( - sectionsWithScales, - unconformityHeight, - axisType, - ); - - /** For each section, find units that are overlapping. - * We do this after merging sections so that we can - * handle cases where there are overlapping units across sections - * */ - const sectionsOut = sections2.map((section) => { - return { - ...section, - units: preprocessUnits(section, axisType), - }; - }); - - /** Reconstitute the units so that they are sorted by section and properly enhanced. - * This is mostly important so that unit keyboard navigation - * predictably selects adjacent units. - */ - const units2 = sectionsOut.reduce((acc, group) => { - const { units } = group; - for (const unit of units) { - acc.push(unit); - } - return acc; - }, []); - - return { - units: units2, - totalHeight, - sections: sectionsOut, - }; -} - interface BaseSurface { index: number; age: number; @@ -272,8 +109,8 @@ interface VariableAgeScaleOptions { export function buildScaleFromSurfaces( surfaces: BaseSurface[], - pixelOffset: number, // height in pixels at which to start the scale - pixelScale: number, // pixels per unit + pixelOffset: number = 0, // height in pixels at which to start the scale + pixelScale: number = 10, // pixels per unit ): PackageScaleInfo { /** Build a variable age scale that places age surfaces equally far apart in height space. * It is presumed that gaps are already removed from the unit set provided. @@ -286,7 +123,7 @@ export function buildScaleFromSurfaces( // Compute the height in pixels for each surface const surfaceHeights = surfaces.map((surface, i) => { - return i * 20; + return i * pixelScale; }); // Build a piecewise linear scale mapping age to pixel height diff --git a/packages/column-views/src/units/composite.ts b/packages/column-views/src/units/composite.ts index 80f295c4..b5f35d17 100644 --- a/packages/column-views/src/units/composite.ts +++ b/packages/column-views/src/units/composite.ts @@ -16,7 +16,7 @@ import { import { BaseUnit } from "@macrostrat/api-types"; import { LabeledUnit, UnitBoxes } from "./boxes"; import styles from "./composite.module.sass"; -import { useMacrostratColumnData } from "../data-provider"; +import { useCompositeScale, useMacrostratColumnData } from "../data-provider"; import { useUnitSelectionDispatch } from "../units"; const h = hyperStyled(styles); @@ -157,13 +157,12 @@ export function SectionLabelsColumn(props: ICompositeUnitProps) { shouldRenderNote, } = props; - const { sections, totalHeight, axisType } = useMacrostratColumnData(); + const { totalHeight, axisType } = useMacrostratColumnData(); + const _compositeScale = useCompositeScale(); const unlabeledUnits = useContext(UnlabeledDivisionsContext); const unitsToLabel = noteMode == "unlabeled" ? unlabeledUnits : undefined; - const _compositeScale = compositeScale(sections); - const selectUnit = useUnitSelectionDispatch(); return h("div.section-labels-column", [ @@ -199,57 +198,12 @@ export function SectionLabelsColumn(props: ICompositeUnitProps) { ]); } -type CompositeScaleOpts = { - clamped?: boolean; -}; - export interface CompositeColumnScale { (val: number): number; copy(): CompositeColumnScale; domain(): number[]; } -export function compositeScale( - sections, - opts: CompositeScaleOpts = {}, -): CompositeColumnScale { - /** A basic composite scale that works across all sections. This isn't a fully featured, - * contiuous D3 scale, but it shares enough attributes to be useful for - * laying out notes. - */ - const { clamped = true } = opts; - - const scales = sections.map((group) => { - const { scaleInfo } = group; - return scaleInfo.scale.copy().clamp(clamped); - }); - - let baseScale: any = (val) => { - // Find the scale for the section that contains the value - const scale = scales.find((scale) => { - return scale.domain()[0] <= val && val <= scale.domain()[1]; - }); - - if (scale) { - return scale(val); - } else { - // return nan - return NaN; - } - }; - - baseScale.copy = () => { - return baseScale; - }; - - baseScale.domain = () => { - // Return a domain that covers all sections - return [scales[0].domain()[0], scales[scales.length - 1].domain()[1]]; - }; - - return baseScale as CompositeColumnScale; -} - export function ColumnNotesProvider(props) { // A fake column axis provider that allows scales to cross const { children, scale, totalHeight, pixelScale } = props; diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index 84f8ef37..36794d06 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -97,6 +97,7 @@ export const EquidistantSurfaces: Story = { unitComponent: ColoredUnitComponent, showTimescale: true, timescaleLevels: [1, 3], - children: h(ComputedSurfacesOverlay), + showUnitPopover: true, + pixelScale: 30, }, }; From 279f247283492681712d57f7b215d0af8384f65b Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Wed, 26 Nov 2025 11:00:14 -0600 Subject: [PATCH 26/46] Replicate ordinal scale for height --- .../src/prepare-units/composite-scale.ts | 14 ++- .../src/prepare-units/dynamic-scales.ts | 87 ++++++++++++++++++- .../stories/nonlinear-scale.stories.ts | 15 ++++ 3 files changed, 110 insertions(+), 6 deletions(-) diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 2101d053..dc1de808 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -5,6 +5,7 @@ import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; import { UnitLong } from "@macrostrat/api-types"; import { buildColumnSurfaces, + buildHybridScale, buildScaleFromSurfaces, HybridScaleType, } from "./dynamic-scales"; @@ -207,13 +208,20 @@ function buildSectionScale( let _pixelScale = opts.pixelScale; let pixelHeight: number; - if (hybridScaleType === HybridScaleType.EquidistantSurfaces) { + if (hybridScaleType != null) { /** In an equidistant surfaces scale, we want to determine the heights of surfaces * and then distribute units evenly between them. + * This is somewhat like an ordinal scale */ - const surfaces = buildColumnSurfaces(data); + if (hybridScaleType == HybridScaleType.EquidistantSurfaces) { + _pixelScale ??= targetUnitHeight; + } - return buildScaleFromSurfaces(surfaces, 0, _pixelScale ?? 20); + return buildHybridScale(data, { + pixelOffset: 0, + pixelScale: _pixelScale, + hybridScaleType, + }); } if (scale == null) { diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index 61aa4805..ce72cc5d 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -107,15 +107,96 @@ interface VariableAgeScaleOptions { domainHeight: number; } -export function buildScaleFromSurfaces( +interface HybridScaleOptions { + pixelOffset?: number; + pixelScale?: number; + hybridScaleType?: HybridScaleType; +} + +export function buildHybridScale( + units: T[], + options: HybridScaleOptions = {}, +): PackageScaleInfo { + const surfaces = buildColumnSurfaces(units); + + if (options.hybridScaleType === HybridScaleType.EquidistantSurfaces) { + return buildScaleFromSurfacesSimple(surfaces, options); + } + + return buildApproximateHeightScale(surfaces, units, options); +} + +export function buildApproximateHeightScale( + surfaces: BaseSurface[], + units: UnitLong[], + options: HybridScaleOptions = {}, +): PackageScaleInfo { + /** Build a variable age scale that places age surfaces equally far apart in height space. + * It is presumed that gaps are already removed from the unit set provided. + * */ + + const { pixelScale = 30, pixelOffset = 0 } = options; + + //return buildScaleFromSurfacesSimple(surfaces, options); + + // Get units associated with each surface + // Note: we could hoist this if it proved useful for other scale types + const domainInfo = getUnitsInAgeDomains(surfaces, units as ExtUnit[]); + + // Compute the height in pixels for each surface + + const surfaceHeights = []; + const ageDomain = []; + let lastHeight = pixelOffset; + + for (const surface of surfaces) { + surfaceHeights.push(lastHeight); + ageDomain.push(surface.age); + lastHeight += pixelScale; + } + + // for (const [i, domain] of domainInfo.entries()) { + // if (i === 0) { + // surfaceHeights.push(lastHeight); + // ageDomain.push(domain.t_age); + // continue; + // } + // + // const thisHeight = pixelScale; + // + // lastHeight += thisHeight; + // surfaceHeights.push(lastHeight); + // ageDomain.push(domain.b_age); + // } + + // Build a piecewise linear scale mapping age to pixel height + const pixelRange = surfaceHeights; + + const scale = scaleLinear().domain(ageDomain).range(pixelRange); + + const domain = [ageDomain[ageDomain.length - 1], ageDomain[0]]; + + return { + scale, + pixelScale: null, // pixels per unit + domain, + pixelHeight: Math.abs(pixelRange[pixelRange.length - 1] - pixelRange[0]), + }; +} + +export function buildScaleFromSurfacesSimple( surfaces: BaseSurface[], - pixelOffset: number = 0, // height in pixels at which to start the scale - pixelScale: number = 10, // pixels per unit + options: HybridScaleOptions = {}, ): PackageScaleInfo { /** Build a variable age scale that places age surfaces equally far apart in height space. * It is presumed that gaps are already removed from the unit set provided. * */ + const { + hybridScaleType = HybridScaleType.EquidistantSurfaces, + pixelScale = 30, + } = options; + const domain: [number, number] = [ surfaces[surfaces.length - 1].age, surfaces[0].age, diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index 36794d06..291ff9cc 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -101,3 +101,18 @@ export const EquidistantSurfaces: Story = { pixelScale: 30, }, }; + +export const HeightScale: Story = { + args: { + id: 448, + // Ordered time bins + axisType: ColumnAxisType.AGE, + hybridScaleType: HybridScaleType.ApproximateHeight, + showLabels: false, + unitComponent: ColoredUnitComponent, + showTimescale: true, + timescaleLevels: [1, 3], + showUnitPopover: true, + children: h(ComputedSurfacesOverlay), + }, +}; From 36d654326de0229b08d2b53c29fac0fe2865e0e0 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Wed, 26 Nov 2025 11:01:47 -0600 Subject: [PATCH 27/46] Replicate ordinal scale again --- .../src/prepare-units/dynamic-scales.ts | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index ce72cc5d..abc62b8d 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -137,8 +137,6 @@ export function buildApproximateHeightScale( const { pixelScale = 30, pixelOffset = 0 } = options; - //return buildScaleFromSurfacesSimple(surfaces, options); - // Get units associated with each surface // Note: we could hoist this if it proved useful for other scale types const domainInfo = getUnitsInAgeDomains(surfaces, units as ExtUnit[]); @@ -149,26 +147,19 @@ export function buildApproximateHeightScale( const ageDomain = []; let lastHeight = pixelOffset; - for (const surface of surfaces) { + for (const domain of domainInfo) { + if (lastHeight == pixelOffset) { + surfaceHeights.push(lastHeight); + ageDomain.push(domain.t_age); + } + + const thisHeight = pixelScale; + + lastHeight += thisHeight; surfaceHeights.push(lastHeight); - ageDomain.push(surface.age); - lastHeight += pixelScale; + ageDomain.push(domain.b_age); } - // for (const [i, domain] of domainInfo.entries()) { - // if (i === 0) { - // surfaceHeights.push(lastHeight); - // ageDomain.push(domain.t_age); - // continue; - // } - // - // const thisHeight = pixelScale; - // - // lastHeight += thisHeight; - // surfaceHeights.push(lastHeight); - // ageDomain.push(domain.b_age); - // } - // Build a piecewise linear scale mapping age to pixel height const pixelRange = surfaceHeights; From 1570bc00877d41788878932c389a409d3a326f09 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Wed, 26 Nov 2025 12:17:19 -0600 Subject: [PATCH 28/46] Basic working height scale --- .../src/prepare-units/dynamic-scales.ts | 98 ++++++++++++++++--- packages/stratigraphy-utils/src/age-ranges.ts | 2 +- 2 files changed, 87 insertions(+), 13 deletions(-) diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index abc62b8d..f21c1d7f 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -2,6 +2,9 @@ import { ExtUnit } from "./helpers"; import { UnitLong } from "@macrostrat/api-types"; import { PackageScaleInfo } from "./composite-scale"; import { scaleLinear } from "d3-scale"; +import { getUnitHeightRange } from "@macrostrat/column-views"; +import { ColumnAxisType } from "@macrostrat/column-components"; +import { mergeAgeRanges, MergeMode } from "@macrostrat/stratigraphy-utils"; export enum HybridScaleType { // An age-domain scale that puts equal vertical space between surfaces @@ -94,17 +97,17 @@ function proportionOfUnitInDomain( b_age: number, ): number { // Compute the proportion of a unit's height that lies within the given age domain - const unitHeight = unit.t_age - unit.b_age; - if (unitHeight <= 0) return 0; - const overlapTop = Math.min(unit.t_age, t_age); - const overlapBottom = Math.max(unit.b_age, b_age); - const overlapHeight = Math.max(0, overlapTop - overlapBottom); - return overlapHeight / unitHeight; -} -interface VariableAgeScaleOptions { - tolerance: number; - domainHeight: number; + const rng = getUnitHeightRange(unit, ColumnAxisType.AGE); + const rng1: [number, number] = [b_age, t_age]; + // Compute overlap + + const mergedRange = mergeAgeRanges([rng, rng1], MergeMode.Inner); + + const unitHeight = Math.abs(rng[1] - rng[0]); + const mergedHeight = Math.abs(mergedRange[1] - mergedRange[0]); + + return mergedHeight / unitHeight; } interface HybridScaleOptions { @@ -126,6 +129,37 @@ export function buildHybridScale( return buildApproximateHeightScale(surfaces, units, options); } +enum HeightMethod { + Minimum = "minimum", + Average = "average", + Maximum = "maximum", +} + +function getApproximateHeight( + unit: ExtUnit, + method: HeightMethod = HeightMethod.Maximum, +): number | null { + // Get approximate height of a unit based on specified method + const heights = []; + let minHeight = unit.min_thick; + if (method === HeightMethod.Minimum || method === HeightMethod.Average) { + const h = unit.min_thick; + if (h != null && !isNaN(h) && h > 0) heights.push(h); + } + if (method === HeightMethod.Average || method === HeightMethod.Maximum) { + const h = unit.max_thick; + if (h != null && !isNaN(h) && h > 0) heights.push(h); + } + + if (heights.length === 0) return null; + + if (method === HeightMethod.Average) { + return heights.reduce((sum, h) => sum + h, 0) / heights.length; + } else { + return heights[0]; + } +} + export function buildApproximateHeightScale( surfaces: BaseSurface[], units: UnitLong[], @@ -135,7 +169,14 @@ export function buildApproximateHeightScale( * It is presumed that gaps are already removed from the unit set provided. * */ - const { pixelScale = 30, pixelOffset = 0 } = options; + const { + pixelScale = 30, + pixelOffset = 0, + minHeight = 5, + heightMethod = HeightMethod.Maximum, + // Default height for when height is unknown + defaultHeight = 100, + } = options; // Get units associated with each surface // Note: we could hoist this if it proved useful for other scale types @@ -153,7 +194,40 @@ export function buildApproximateHeightScale( ageDomain.push(domain.t_age); } - const thisHeight = pixelScale; + // Approximate height based on units in this domain + const units = domain.units; + let unitHeightInfo: { unit: ExtUnit; height: number }[] = []; + + for (const unit of units) { + const proportion = proportionOfUnitInDomain( + unit, + domain.t_age, + domain.b_age, + ); + const height = getApproximateHeight(unit, heightMethod); + if (height != null) { + unitHeightInfo.push({ unit, height: height * proportion }); + } + } + + let thisHeight = 0; + if (unitHeightInfo.length === 0) { + thisHeight = pixelScale; // Default height if no units with height info + } else { + // Normalize weights (take the mean) + + const meanHeight = + unitHeightInfo.reduce((sum, d) => sum + d.height, 0) / + unitHeightInfo.length; + + if (meanHeight == 0) { + thisHeight = defaultHeight; // Arbitrary height for zero-height intervals + } else if (meanHeight < minHeight) { + thisHeight = minHeight; // Minimum height + } else { + thisHeight = meanHeight; + } + } lastHeight += thisHeight; surfaceHeights.push(lastHeight); diff --git a/packages/stratigraphy-utils/src/age-ranges.ts b/packages/stratigraphy-utils/src/age-ranges.ts index ddfd648d..1ab78f11 100644 --- a/packages/stratigraphy-utils/src/age-ranges.ts +++ b/packages/stratigraphy-utils/src/age-ranges.ts @@ -1,6 +1,6 @@ export type AgeRange = [number, number]; -enum MergeMode { +export enum MergeMode { Inner, Outer, } From 6eb19ad9e8c01fddb09bca0969f12f2c364a5663 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Wed, 26 Nov 2025 14:06:42 -0600 Subject: [PATCH 29/46] Getting closer on timescale --- packages/column-components/src/axis.ts | 3 +- .../src/prepare-units/composite-scale.ts | 8 ++- .../src/prepare-units/dynamic-scales.ts | 22 +++++++- .../stories/nonlinear-scale.stories.ts | 17 ++++++ packages/timescale/src/components/index.ts | 17 ++++-- packages/timescale/src/index.ts | 30 ++--------- packages/timescale/src/provider.ts | 54 +++++++++++++++++-- 7 files changed, 111 insertions(+), 40 deletions(-) diff --git a/packages/column-components/src/axis.ts b/packages/column-components/src/axis.ts index 3867c552..918a48c3 100644 --- a/packages/column-components/src/axis.ts +++ b/packages/column-components/src/axis.ts @@ -50,7 +50,8 @@ export function AgeAxis(props: AgeAxisProps) { } = props; const range = scale.range(); - const pixelHeight = Math.abs(range[0] - range[1]); + + const pixelHeight = Math.abs(range[0] - range[range.length - 1]); let tickValues: number[] = undefined; diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index dc1de808..87baee82 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -217,7 +217,9 @@ function buildSectionScale( _pixelScale ??= targetUnitHeight; } - return buildHybridScale(data, { + console.log("Section scale", domain, scale?.domain()); + + return buildHybridScale(data, domain, { pixelOffset: 0, pixelScale: _pixelScale, hybridScaleType, @@ -279,6 +281,10 @@ export function createPackageScale( .range(range0.map((d) => d + offset)); } + _scale.clamp(); + + console.log("Created package scale", domain, pixelHeight, _scale.domain()); + return { domain, pixelScale, diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index f21c1d7f..366d1daa 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -118,15 +118,33 @@ interface HybridScaleOptions { export function buildHybridScale( units: T[], + domain: [number, number], options: HybridScaleOptions = {}, ): PackageScaleInfo { const surfaces = buildColumnSurfaces(units); + console.log("Surfaces:", surfaces, domain); + + const filteredSurfaces = surfaces.filter( + (s) => s.age < Math.max(...domain) && s.age > Math.min(...domain), + ); + + const s1 = [ + { index: -1, age: Math.min(...domain), units_above: [], units_below: [] }, + ...filteredSurfaces, + { + index: -1, + age: Math.max(...domain), + units_above: [], + units_below: [], + }, + ]; + if (options.hybridScaleType === HybridScaleType.EquidistantSurfaces) { - return buildScaleFromSurfacesSimple(surfaces, options); + return buildScaleFromSurfacesSimple(s1, options); } - return buildApproximateHeightScale(surfaces, units, options); + return buildApproximateHeightScale(s1, units, options); } enum HeightMethod { diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index 291ff9cc..b3bfcc04 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -116,3 +116,20 @@ export const HeightScale: Story = { children: h(ComputedSurfacesOverlay), }, }; + +export const KentuckyOrdovician: Story = { + args: { + id: 448, + // Ordered time bins + axisType: ColumnAxisType.AGE, + hybridScaleType: HybridScaleType.ApproximateHeight, + showLabels: false, + unitComponent: ColoredUnitComponent, + showTimescale: true, + t_age: 430, + b_age: 490, + timescaleLevels: [1, 3], + showUnitPopover: true, + children: h(ComputedSurfacesOverlay), + }, +}; diff --git a/packages/timescale/src/components/index.ts b/packages/timescale/src/components/index.ts index 7b668cd0..e1af8bc8 100644 --- a/packages/timescale/src/components/index.ts +++ b/packages/timescale/src/components/index.ts @@ -95,11 +95,22 @@ function TimescaleBoxes(props: { // This age range extends further than any realistic constraints const expandedAgeRange = ensureIncreasingAgeRange(ageRange) ?? [-50, 5000]; + console.log(scale.domain(), ageRange); + // If we have a scale, give us the boundaries clipped to the age range if appropriate + + // Don't render if we are fully outside the age range of interest + if (eag < expandedAgeRange[0]) return null; + if (lag > expandedAgeRange[expandedAgeRange.length - 1]) return null; + if (scale != null) { - const startAge = Math.min(expandedAgeRange[1], eag); + const startAge = Math.min( + expandedAgeRange[expandedAgeRange.length - 1], + eag, + ); const endAge = Math.max(expandedAgeRange[0], lag); length = Math.abs(scale(startAge) - scale(endAge)); + console.log(interval.nam, startAge, endAge, length); } let style = {}; @@ -113,10 +124,6 @@ function TimescaleBoxes(props: { const { children, nam: name } = interval; - // Don't render if we are fully outside the age range of interest - if (eag < expandedAgeRange[0]) return null; - if (lag > expandedAgeRange[1]) return null; - const className = slugify(name); return h("div.interval", { className, style }, [ diff --git a/packages/timescale/src/index.ts b/packages/timescale/src/index.ts index 461ed931..34ae9df0 100644 --- a/packages/timescale/src/index.ts +++ b/packages/timescale/src/index.ts @@ -76,7 +76,6 @@ function Timescale(props: TimescaleProps) { orientation = TimescaleOrientation.HORIZONTAL, ageRange, length: l, - absoluteAgeScale = false, showAgeAxis = true, levels, scale, @@ -96,30 +95,6 @@ function Timescale(props: TimescaleProps) { ); const className = classNames(orientation, "increase-" + increaseDirection); - const length = absoluteAgeScale ? (l ?? 6000) : null; - - let ageRange2 = null; - if (ageRange != null) { - ageRange2 = [...ageRange]; - } - if (ageRange2 == null) { - ageRange2 = [timescale.eag, timescale.lag]; - } - if ( - orientation == TimescaleOrientation.VERTICAL && - increaseDirection == IncreaseDirection.DOWN_LEFT && - ageRange2[0] < ageRange2[1] - ) { - ageRange2.reverse(); - } - - let length2 = l; - - if (scale != null) { - ageRange2 = scale.domain() as [number, number]; - const rng = scale.range(); - length2 = Math.abs(rng[1] - rng[0]); - } return h( TimescaleProvider, @@ -127,11 +102,12 @@ function Timescale(props: TimescaleProps) { timescale, selectedInterval: null, parentMap, - ageRange: ageRange2, - length: length2, + ageRange: ageRange, + length: l, orientation, levels, scale, + increaseDirection, }, h(TimescaleContainer, { className }, [ h(TimescaleBoxes, { diff --git a/packages/timescale/src/provider.ts b/packages/timescale/src/provider.ts index d9c4ff1d..6d21e7a2 100644 --- a/packages/timescale/src/provider.ts +++ b/packages/timescale/src/provider.ts @@ -1,12 +1,51 @@ import h from "@macrostrat/hyper"; import { scaleLinear } from "@visx/scale"; import { createContext, useContext } from "react"; -import { TimescaleCTX } from "./types"; +import { TimescaleCTX, TimescaleOrientation } from "./types"; +import { IncreaseDirection } from "./index"; const TimescaleContext = createContext(null); -function TimescaleProvider(props: React.PropsWithChildren) { - const { children, timescale, ageRange, length, scale, ...rest } = props; +interface TimescaleProviderProps extends TimescaleCTX { + children: React.ReactNode; + increaseDirection?: IncreaseDirection; +} + +function TimescaleProvider(props: TimescaleProviderProps) { + const { + children, + timescale, + ageRange, + length, + scale, + increaseDirection, + orientation, + ...rest + } = props; + + let ageRange2 = null; + if (ageRange != null) { + ageRange2 = [...ageRange]; + } + if (ageRange2 == null) { + ageRange2 = [timescale.eag, timescale.lag]; + } + if ( + orientation == TimescaleOrientation.VERTICAL && + increaseDirection == IncreaseDirection.DOWN_LEFT && + ageRange2[0] < ageRange2[1] + ) { + ageRange2.reverse(); + } + + let length2 = length; + + if (scale != null) { + let _domain = scale.domain() as number[]; + ageRange2 = [Math.min(..._domain), Math.max(..._domain)]; + const rng = scale.range(); + length2 = Math.abs(rng[rng.length - 1] - rng[0]); + } let scale2 = scale; if (length && ageRange && scale2 == null) { @@ -16,7 +55,14 @@ function TimescaleProvider(props: React.PropsWithChildren) { }); } - const value = { ...rest, scale: scale2, timescale, ageRange, length }; + const value = { + ...rest, + scale: scale2, + orientation, + timescale, + ageRange: ageRange2, + length: length2, + }; return h(TimescaleContext.Provider, { value }, children); } From 8ca1acf1b3a2cac546917319da2497a441827499 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Thu, 27 Nov 2025 01:51:13 -0600 Subject: [PATCH 30/46] Add timescale option to get intervals from Macrostrat API Fixes #182 --- packages/api-types/src/defs.d.ts | 12 +- .../column-views/src/data-provider/base.ts | 13 +- .../src/prepare-units/dynamic-scales.ts | 2 - packages/column-views/src/section.ts | 13 +- .../stories/nonlinear-scale.stories.ts | 4 +- packages/timescale/src/components/index.ts | 3 +- packages/timescale/src/index.ts | 17 ++- packages/timescale/src/intervals-api.ts | 120 ++++++++++++++++++ 8 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 packages/timescale/src/intervals-api.ts diff --git a/packages/api-types/src/defs.d.ts b/packages/api-types/src/defs.d.ts index fbd31f99..5f21081b 100644 --- a/packages/api-types/src/defs.d.ts +++ b/packages/api-types/src/defs.d.ts @@ -23,12 +23,20 @@ export interface MacrostratRef { t_units: number; } -export type Interval = { +export type TimescaleRef = { + timescale_id: number; + name: string; +}; + +export type MacrostratInterval = { int_id: number; name: string; abbrev?: string; t_age?: number; b_age?: number; - timescales?: string[]; + int_type?: string; + timescales?: TimescaleRef[]; color: string; }; + +export type Interval = MacrostratInterval; diff --git a/packages/column-views/src/data-provider/base.ts b/packages/column-views/src/data-provider/base.ts index d57da71e..b448bc6f 100644 --- a/packages/column-views/src/data-provider/base.ts +++ b/packages/column-views/src/data-provider/base.ts @@ -268,10 +268,21 @@ export function useMacrostratStore(selector: MacrostratSelector | "api") { if (selector === "api") { return ctx; } - return useStore(ctx, selector); } +export function useMacrostratBaseURL( + defaultURL = "https://macrostrat.org/api/v2", +): string { + /** Get the Macrostrat base URL from the store if set, otherwise return a default value */ + const ctx = useContext(MacrostratDataProviderContext); + if (ctx == null) { + // Return default URL if no provider is present + return defaultURL; + } + return ctx.getState().baseURL; +} + type DataTypeKey = | "lithologies" | "intervals" diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index 366d1daa..06e58092 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -123,8 +123,6 @@ export function buildHybridScale( ): PackageScaleInfo { const surfaces = buildColumnSurfaces(units); - console.log("Surfaces:", surfaces, domain); - const filteredSurfaces = surfaces.filter( (s) => s.age < Math.max(...domain) && s.age > Math.min(...domain), ); diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index 0c6ea205..ccf1253e 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -4,7 +4,11 @@ import { SectionLabelsColumn, } from "./units"; import { ReactNode, FunctionComponent, useMemo } from "react"; -import { Timescale, TimescaleOrientation } from "@macrostrat/timescale"; +import { + Timescale, + TimescaleOrientation, + useMacrostratIntervals, +} from "@macrostrat/timescale"; import { ColumnAxisType, SVG } from "@macrostrat/column-components"; import hyper from "@macrostrat/hyper"; import styles from "./column.module.sass"; @@ -14,6 +18,7 @@ import { useMacrostratColumnData, useMacrostratUnits, MacrostratColumnProvider, + useMacrostratBaseURL, } from "./data-provider"; import { Duration } from "./unit-details"; import { Value } from "@macrostrat/data-components"; @@ -217,6 +222,10 @@ type CompositeTimescaleCoreProps = CompositeTimescaleProps & { export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { const { levels = 3, packages, unconformityLabels = false } = props; + // Use intervals from Macrostrat API + const baseURL = useMacrostratBaseURL(); + const intervals = useMacrostratIntervals(baseURL); + let _levels: [number, number]; if (typeof levels === "number") { // If levels is a number, use the most common starting level @@ -243,7 +252,7 @@ export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { absoluteAgeScale: true, showAgeAxis: false, scale, - //ageRange: domain as [number, number], + intervals, }), ], ); diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index b3bfcc04..e74394b5 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -126,10 +126,10 @@ export const KentuckyOrdovician: Story = { showLabels: false, unitComponent: ColoredUnitComponent, showTimescale: true, - t_age: 430, + t_age: 455, b_age: 490, timescaleLevels: [1, 3], showUnitPopover: true, - children: h(ComputedSurfacesOverlay), + //children: h(ComputedSurfacesOverlay), }, }; diff --git a/packages/timescale/src/components/index.ts b/packages/timescale/src/components/index.ts index e1af8bc8..01ca1a90 100644 --- a/packages/timescale/src/components/index.ts +++ b/packages/timescale/src/components/index.ts @@ -95,7 +95,7 @@ function TimescaleBoxes(props: { // This age range extends further than any realistic constraints const expandedAgeRange = ensureIncreasingAgeRange(ageRange) ?? [-50, 5000]; - console.log(scale.domain(), ageRange); + console.log(scale.domain(), scale.range(), ageRange); // If we have a scale, give us the boundaries clipped to the age range if appropriate @@ -111,6 +111,7 @@ function TimescaleBoxes(props: { const endAge = Math.max(expandedAgeRange[0], lag); length = Math.abs(scale(startAge) - scale(endAge)); console.log(interval.nam, startAge, endAge, length); + console.log(scale(startAge), scale(endAge)); } let style = {}; diff --git a/packages/timescale/src/index.ts b/packages/timescale/src/index.ts index 34ae9df0..7b7d8027 100644 --- a/packages/timescale/src/index.ts +++ b/packages/timescale/src/index.ts @@ -11,7 +11,7 @@ import { nestTimescale } from "./preprocess"; import { AgeAxis, AgeAxisProps } from "./age-axis"; import classNames from "classnames"; import { ScaleContinuousNumeric } from "d3-scale"; -import { useEffect, useMemo } from "react"; +import { useMemo } from "react"; import h from "./hyper"; type ClickHandler = (event: Event, interval: any) => void; @@ -89,13 +89,19 @@ function Timescale(props: TimescaleProps) { labelProps, } = props; - const [parentMap, timescale] = useMemo( - () => nestTimescale(rootInterval, intervals), - [rootInterval, intervals], - ); + const [parentMap, timescale] = useMemo(() => { + if (intervals.length == 0) { + return [null, null]; + } + return nestTimescale(rootInterval, intervals); + }, [rootInterval, intervals]); const className = classNames(orientation, "increase-" + increaseDirection); + if (parentMap == null || timescale == null) { + return null; + } + return h( TimescaleProvider, { @@ -122,6 +128,7 @@ function Timescale(props: TimescaleProps) { ); } +export * from "./intervals-api"; export { Timescale, TimescaleOrientation, diff --git a/packages/timescale/src/intervals-api.ts b/packages/timescale/src/intervals-api.ts new file mode 100644 index 00000000..5430fbf9 --- /dev/null +++ b/packages/timescale/src/intervals-api.ts @@ -0,0 +1,120 @@ +/** Helpers to fetch Macrostrat intervals from the API. + * + * TODO: integrate with MacrostratColumnDataProvider to provide intervals via context. + * */ + +import { MacrostratInterval } from "@macrostrat/api-types"; +import { defaultIntervals } from "./intervals"; +import { useState } from "react"; +import { useAsyncEffect } from "@macrostrat/ui-components"; +import { Interval } from "./types"; + +export async function fetchMacrostratIntervals( + baseUrl: string, + // Default to ICS timescale + timescaleID?: number = 11, +): Promise { + const url = new URL(`${baseUrl}/defs/intervals`); + url.searchParams.set("timescale_id", timescaleID.toString()); + const response = await fetch(url.toString()); + + if (!response.ok) { + throw new Error(`Failed to fetch intervals: ${response.statusText}`); + } + + const res = await response.json(); + + const data = res.success?.data; + + if (!Array.isArray(data)) { + throw new Error("Invalid data received from API"); + } + + return data as MacrostratInterval[]; +} + +export function buildIntervalsTree( + intervals: MacrostratInterval[], +): Interval[] { + // Geologic time + const rootInterval: Interval = defaultIntervals[0]; + + const levels = [1, 2, 3, 4, 5]; + const levelMap = new Map(); + for (const level of levels) { + levelMap.set(level, []); + } + + for (const interval of intervals) { + const level = getIntervalLevel(interval); + if (level != null) { + levelMap.get(level).push(interval); + } + } + + const output = [rootInterval]; // Geologic time + for (const [lvl, entries] of levelMap.entries()) { + const levelIntervals: Interval[] = []; + const parentLevel = lvl - 1; + const parentIntervals = levelMap.get(parentLevel); + for (const int of entries) { + // Find parent interval + let pid: number; + if (parentLevel === 0) { + pid = 0; + } else { + pid = parentIntervals.find((parentInt) => { + return int.t_age >= parentInt.t_age && int.b_age <= parentInt.b_age; + })?.int_id; + if (pid == null) { + console.warn( + `No parent found for interval ${int.name} (level ${lvl})`, + ); + continue; + } + } + levelIntervals.push({ + oid: int.int_id, + typ: "int", + lvl, + nam: int.name, + eag: int.b_age, + lag: int.t_age, + pid: pid, + col: int.color, + int_id: int.int_id, + }); + } + // sort level intervals by t_age descending + levelIntervals.sort((a, b) => b.eag - a.eag); + output.push(...levelIntervals); + } + return output; +} + +function getIntervalLevel(interval: MacrostratInterval): number { + const levelMap: { [key: string]: number } = { + eon: 1, + era: 2, + period: 3, + epoch: 4, + age: 5, + }; + + return levelMap[interval.int_type.toLowerCase()]; +} + +export function useMacrostratIntervals( + baseURL = "https://macrostrat.org/api/v2", +): Interval[] { + /** Get a stratified tree of ICS intervals from the Macrostrat API. */ + const [intervals, setIntervals] = useState([]); + + useAsyncEffect(async () => { + const fetchedIntervals = await fetchMacrostratIntervals(baseURL, 11); + const intervalTree = buildIntervalsTree(fetchedIntervals); + setIntervals(intervalTree); + }, [baseURL]); + + return intervals; +} From b64e71dc88ccd0e65e69792f10ff27477323c271 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Thu, 27 Nov 2025 05:27:24 -0600 Subject: [PATCH 31/46] Partially improve age axis --- .../column-views/src/age-axis.module.sass | 6 +- packages/column-views/src/age-axis.ts | 60 +++++++++++++++---- .../src/prepare-units/composite-scale.ts | 14 ++++- .../src/prepare-units/dynamic-scales.ts | 16 +++-- .../stories/nonlinear-scale.stories.ts | 43 ++++++++++++- packages/timescale/src/components/index.ts | 32 +++++----- 6 files changed, 133 insertions(+), 38 deletions(-) diff --git a/packages/column-views/src/age-axis.module.sass b/packages/column-views/src/age-axis.module.sass index 3101bc98..b646ea5f 100644 --- a/packages/column-views/src/age-axis.module.sass +++ b/packages/column-views/src/age-axis.module.sass @@ -42,6 +42,8 @@ background-color: var(--column-background-color) text-align: center +.composite-age-axis + position: relative + display: flex + flex-direction: row - - diff --git a/packages/column-views/src/age-axis.ts b/packages/column-views/src/age-axis.ts index cdbff37a..9870acd1 100644 --- a/packages/column-views/src/age-axis.ts +++ b/packages/column-views/src/age-axis.ts @@ -41,7 +41,12 @@ export function VerticalAxisLabel(props) { ); } -export function CompositeAgeAxis() { +interface CompositeAgeAxisProps { + className?: string; + style?: React.CSSProperties; +} + +export function CompositeAgeAxis(rest: CompositeAgeAxisProps) { const { axisType, sections, totalHeight } = useMacrostratColumnData(); const packages = sections.map((section) => { @@ -55,36 +60,67 @@ export function CompositeAgeAxis() { axisType, packages, totalHeight, + ...rest, + }); +} + +export function ApproximateHeightAxis(rest: CompositeAgeAxisProps) { + /** Axis to show approximate height based on dynamic column scales */ + const { axisType, sections, totalHeight } = useMacrostratColumnData(); + + const packages = sections.map((section) => { + const { scaleInfo } = section; + if (scaleInfo.heightScale == null) { + throw new Error("No height scale available for section"); + } + return { + key: `section-${section.section_id}`, + ...scaleInfo, + scale: scaleInfo.heightScale, // Use height scale instead of age scale + // This only works with dynamic columns + }; + }); + + return h(CompositeAgeAxisCore, { + axisType, + axisLabel: "Approx. height", + axisUnit: "m", + packages, + totalHeight, + ...rest, }); } -export interface CompositeStratigraphicScaleInfo { +export interface CompositeStratigraphicScaleInfo extends CompositeAgeAxisProps { axisType: ColumnAxisType; + axisLabel?: string; + axisUnit?: string; totalHeight: number; packages: PackageScaleLayoutData[]; } export function CompositeAgeAxisCore(props: CompositeStratigraphicScaleInfo) { - const { axisType, totalHeight, packages } = props; + const { axisType, axisLabel, axisUnit, totalHeight, packages, ...rest } = + props; if (axisType == ColumnAxisType.ORDINAL) { return null; } - let axisLabel: string = "Age"; - let axisUnit = "Ma"; + let _axisLabel: string = axisLabel ?? "Age"; + let _axisUnit = axisUnit ?? "Ma"; if (axisType == ColumnAxisType.DEPTH) { - axisLabel = "Depth"; - axisUnit = "m"; + _axisLabel = "Depth"; + _axisUnit = "m"; } else if (axisType == ColumnAxisType.HEIGHT) { - axisLabel = "Height"; - axisUnit = "m"; + _axisLabel = "Height"; + _axisUnit = "m"; } - return h([ + return h("div.composite-age-axis", rest, [ h(VerticalAxisLabel, { - label: axisLabel, - unit: axisUnit, + label: _axisLabel, + unit: _axisUnit, height: totalHeight, }), h( diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 87baee82..3237bf16 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -49,6 +49,8 @@ export interface PackageScaleInfo { // TODO: add a function scale: ScaleContinuousNumeric; pixelScale?: number; // if it's a linear scale, this could be defined + // Subsidiary scale for height mapping (for hybrid scales) + heightScale?: ScaleContinuousNumeric; } export type PackageScaleLayoutData = PackageScaleInfo & { @@ -261,7 +263,7 @@ export function createPackageScale( ): PackageScaleInfo { /** Build a section scale */ // Domain should be oriented from bottom to top, but scale is oriented from top to bottom - const { domain, pixelScale, pixelHeight, scale } = def; + const { domain, pixelScale, pixelHeight, scale, heightScale } = def; if (scale == null && pixelScale == null) { throw new Error("Either scale or pixelScale must be provided"); @@ -283,13 +285,21 @@ export function createPackageScale( _scale.clamp(); - console.log("Created package scale", domain, pixelHeight, _scale.domain()); + let _heightScale = null; + if (heightScale != null) { + // Adjust height scale as well + const range0 = scale.range(); + _heightScale = heightScale.copy().range(range0.map((d) => d + offset)); + _heightScale.clamp(); + } return { domain, pixelScale, pixelHeight, scale: _scale, + // Internal details for hybrid scales. TODO: improve this + heightScale: _heightScale, }; } diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index 06e58092..c0c10dbe 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -186,7 +186,7 @@ export function buildApproximateHeightScale( * */ const { - pixelScale = 30, + pixelScale = 1, // pixels per meter pixelOffset = 0, minHeight = 5, heightMethod = HeightMethod.Maximum, @@ -228,7 +228,7 @@ export function buildApproximateHeightScale( let thisHeight = 0; if (unitHeightInfo.length === 0) { - thisHeight = pixelScale; // Default height if no units with height info + thisHeight = defaultHeight; // Default height if no units with height info } else { // Normalize weights (take the mean) @@ -251,17 +251,25 @@ export function buildApproximateHeightScale( } // Build a piecewise linear scale mapping age to pixel height - const pixelRange = surfaceHeights; + const pixelRange = surfaceHeights.map((h) => h * pixelScale); const scale = scaleLinear().domain(ageDomain).range(pixelRange); - const domain = [ageDomain[ageDomain.length - 1], ageDomain[0]]; + const heightDomain = surfaceHeights.map((h) => lastHeight - h); + + const heightScale = scaleLinear().domain(heightDomain).range(pixelRange); + + const domain: [number, number] = [ + ageDomain[ageDomain.length - 1], + ageDomain[0], + ]; return { scale, pixelScale: null, // pixels per unit domain, pixelHeight: Math.abs(pixelRange[pixelRange.length - 1] - pixelRange[0]), + heightScale, }; } diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index e74394b5..f0d6a9f3 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -3,7 +3,9 @@ import styles from "./column.stories.module.sass"; import { Meta, StoryObj } from "@storybook/react-vite"; import { + ApproximateHeightAxis, ColoredUnitComponent, + CompositeAgeAxis, ComputedSurfacesOverlay, MergeSectionsMode, } from "@macrostrat/column-views"; @@ -130,6 +132,45 @@ export const KentuckyOrdovician: Story = { b_age: 490, timescaleLevels: [1, 3], showUnitPopover: true, - //children: h(ComputedSurfacesOverlay), + }, +}; + +export const WithApproximateHeightScale: Story = { + args: { + id: 448, + // Ordered time bins + axisType: ColumnAxisType.AGE, + hybridScaleType: HybridScaleType.ApproximateHeight, + showLabels: false, + unitComponent: ColoredUnitComponent, + showTimescale: true, + t_age: 455, + b_age: 490, + timescaleLevels: [1, 3], + showUnitPopover: true, + children: h(ApproximateHeightAxis, { + // Move to the left side + style: { order: -1, marginRight: "8px" }, + }), + }, +}; + +export const WithApproximateHeightScaleOnly: Story = { + args: { + id: 448, + // Ordered time bins + pixelScale: 0.5, + axisType: ColumnAxisType.AGE, + hybridScaleType: HybridScaleType.ApproximateHeight, + showLabels: false, + unitComponent: ColoredUnitComponent, + showUnitPopover: true, + showTimescale: false, + unconformityHeight: 20, + unconformityLabels: false, + children: h(ApproximateHeightAxis, { + // Move to the left side + style: { order: -1, marginRight: "8px" }, + }), }, }; diff --git a/packages/timescale/src/components/index.ts b/packages/timescale/src/components/index.ts index 01ca1a90..db2c2235 100644 --- a/packages/timescale/src/components/index.ts +++ b/packages/timescale/src/components/index.ts @@ -1,8 +1,9 @@ import h from "../hyper"; -import { useRef, useEffect, useState } from "react"; +import { useMemo, useState } from "react"; import { Interval, NestedInterval, TimescaleOrientation } from "../types"; import { useTimescale } from "../provider"; import { SizeAwareLabel } from "@macrostrat/ui-components"; +import classNames from "classnames"; import { CSSProperties } from "react"; @@ -24,39 +25,40 @@ function IntervalBox(props: { allowLabelRotation?: boolean; onClick: (e: Event, interval: Interval) => void; }) { - const { interval, intervalStyle, onClick, labelProps } = props; + const { interval, intervalStyle, onClick, labelProps = {} } = props; const [labelText, setLabelText] = useState(interval.nam); + const _onClick = useMemo(() => { + if (onClick == null) return null; + return (e) => onClick(e, interval); + }, [onClick, interval]); + let style: CSSProperties = {}; if (typeof intervalStyle === "function") { style = intervalStyle(interval); } else if (intervalStyle != null) { style = intervalStyle; } + style.backgroundColor = interval.col; - style = { backgroundColor: interval.col, ...style }; - - // if (backgroundColor != null && (color == null || borderColor == null)) { - // const base = chroma(backgroundColor); - // color ??= base.darken(0.3); - // borderColor ??= base.darken(-0.1); - // } + const className = classNames("interval-box", { + clickable: onClick != null, + }); return h(SizeAwareLabel, { key: interval.oid, style, - className: - "interval-box " + (onClick && interval.int_id != null ? "clickable" : ""), + className, labelClassName: "interval-label", label: labelText, - ...(labelProps ?? {}), + ...labelProps, onVisibilityChanged(viz) { if (!viz && labelText.length > 1) { setLabelText(labelText[0]); } }, - onClick: (e) => onClick(e, interval), + onClick: _onClick, }); } @@ -95,8 +97,6 @@ function TimescaleBoxes(props: { // This age range extends further than any realistic constraints const expandedAgeRange = ensureIncreasingAgeRange(ageRange) ?? [-50, 5000]; - console.log(scale.domain(), scale.range(), ageRange); - // If we have a scale, give us the boundaries clipped to the age range if appropriate // Don't render if we are fully outside the age range of interest @@ -110,8 +110,6 @@ function TimescaleBoxes(props: { ); const endAge = Math.max(expandedAgeRange[0], lag); length = Math.abs(scale(startAge) - scale(endAge)); - console.log(interval.nam, startAge, endAge, length); - console.log(scale(startAge), scale(endAge)); } let style = {}; From 52a0b3d47f44927d908d93727b4f93131cf01b27 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Thu, 27 Nov 2025 12:02:11 -0600 Subject: [PATCH 32/46] Separate hybrid scale props --- packages/column-views/src/age-axis.ts | 3 +- packages/column-views/src/column.ts | 5 ++- .../src/prepare-units/composite-scale.ts | 18 ++++---- .../src/prepare-units/dynamic-scales.ts | 41 ++++++++++++------- .../column-views/src/prepare-units/index.ts | 4 +- .../stories/nonlinear-scale.stories.ts | 16 ++++++-- 6 files changed, 53 insertions(+), 34 deletions(-) diff --git a/packages/column-views/src/age-axis.ts b/packages/column-views/src/age-axis.ts index 9870acd1..ad6ecec4 100644 --- a/packages/column-views/src/age-axis.ts +++ b/packages/column-views/src/age-axis.ts @@ -70,6 +70,7 @@ export function ApproximateHeightAxis(rest: CompositeAgeAxisProps) { const packages = sections.map((section) => { const { scaleInfo } = section; + console.log(scaleInfo); if (scaleInfo.heightScale == null) { throw new Error("No height scale available for section"); } @@ -150,8 +151,6 @@ export function AgeCursor({ age }) { const scale = useCompositeScale(); const heightPx = scale(age); - console.log(age, heightPx); - if (age == null || heightPx == null) { return null; } diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 6589b7ff..4b65473c 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -104,7 +104,7 @@ export function Column(props: ColumnProps) { minSectionHeight = 50, collapseSmallUnconformities = true, allowUnitSelection, - hybridScaleType, + hybridScale, scale, ...rest } = props; @@ -135,8 +135,9 @@ export function Column(props: ColumnProps) { minPixelScale: _minPixelScale, minSectionHeight: _minSectionHeight, collapseSmallUnconformities, - hybridScaleType, + // TODO: consider unifying scale and hybridScale options scale, + hybridScale, }); if (sections.length === 0) { diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 3237bf16..5c876648 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -4,9 +4,8 @@ import { ensureArray, getUnitHeightRange } from "./utils"; import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; import { UnitLong } from "@macrostrat/api-types"; import { - buildColumnSurfaces, buildHybridScale, - buildScaleFromSurfaces, + HybridScaleDefinition, HybridScaleType, } from "./dynamic-scales"; @@ -31,7 +30,9 @@ export interface ColumnHeightScaleOptions { // A continuous scale to use instead of generating one // TODO: discontinuous scales are not yet supported scale?: ScaleContinuousNumeric; - hybridScaleType?: HybridScaleType; + // Hybrid scale type. + // This overrides parameters such as the axis type + hybridScale?: HybridScaleDefinition; } export interface SectionScaleOptions extends ColumnHeightScaleOptions { @@ -201,7 +202,7 @@ function buildSectionScale( axisType, minSectionHeight, scale, - hybridScaleType, + hybridScale, } = opts; const domain = opts.domain ?? findSectionHeightRange(data, axisType); @@ -210,21 +211,18 @@ function buildSectionScale( let _pixelScale = opts.pixelScale; let pixelHeight: number; - if (hybridScaleType != null) { + if (hybridScale != null) { /** In an equidistant surfaces scale, we want to determine the heights of surfaces * and then distribute units evenly between them. * This is somewhat like an ordinal scale */ - if (hybridScaleType == HybridScaleType.EquidistantSurfaces) { + if (hybridScale.type === HybridScaleType.EquidistantSurfaces) { _pixelScale ??= targetUnitHeight; } - console.log("Section scale", domain, scale?.domain()); - - return buildHybridScale(data, domain, { + return buildHybridScale(hybridScale, data, domain, { pixelOffset: 0, pixelScale: _pixelScale, - hybridScaleType, }); } diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index c0c10dbe..af86eb26 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -13,6 +13,25 @@ export enum HybridScaleType { ApproximateHeight = "approximate-height", } +interface HybridScaleOptions { + pixelOffset?: number; + pixelScale?: number; +} + +type ApproxHeightScaleOptions = { + minHeight?: number; + defaultHeight?: number; + heightMethod?: HeightMethod; +}; + +export type HybridScaleDefinition = + | ({ + type: HybridScaleType.ApproximateHeight; + } & ApproxHeightScaleOptions) + | { + type: HybridScaleType.EquidistantSurfaces; + }; + interface BaseSurface { index: number; age: number; @@ -110,13 +129,8 @@ function proportionOfUnitInDomain( return mergedHeight / unitHeight; } -interface HybridScaleOptions { - pixelOffset?: number; - pixelScale?: number; - hybridScaleType?: HybridScaleType; -} - export function buildHybridScale( + def: HybridScaleDefinition, units: T[], domain: [number, number], options: HybridScaleOptions = {}, @@ -138,14 +152,16 @@ export function buildHybridScale( }, ]; - if (options.hybridScaleType === HybridScaleType.EquidistantSurfaces) { + const { type, ...rest } = def; + + if (type === HybridScaleType.EquidistantSurfaces) { return buildScaleFromSurfacesSimple(s1, options); } - return buildApproximateHeightScale(s1, units, options); + return buildApproximateHeightScale(s1, units, { ...options, ...rest }); } -enum HeightMethod { +export enum HeightMethod { Minimum = "minimum", Average = "average", Maximum = "maximum", @@ -179,7 +195,7 @@ function getApproximateHeight( export function buildApproximateHeightScale( surfaces: BaseSurface[], units: UnitLong[], - options: HybridScaleOptions = {}, + options: HybridScaleOptions & ApproxHeightScaleOptions = {}, ): PackageScaleInfo { /** Build a variable age scale that places age surfaces equally far apart in height space. * It is presumed that gaps are already removed from the unit set provided. @@ -281,10 +297,7 @@ export function buildScaleFromSurfacesSimple( * It is presumed that gaps are already removed from the unit set provided. * */ - const { - hybridScaleType = HybridScaleType.EquidistantSurfaces, - pixelScale = 30, - } = options; + const { pixelScale = 30 } = options; const domain: [number, number] = [ surfaces[surfaces.length - 1].age, diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 26a54a27..27eed2b8 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -52,7 +52,7 @@ export function prepareColumnUnits( axisType, unconformityHeight, collapseSmallUnconformities = false, - hybridScaleType, + hybridScale, scale, } = options; @@ -159,7 +159,7 @@ export function prepareColumnUnits( */ let sectionsWithScales = computeSectionHeights(sections, options); - if (collapseSmallUnconformities && hybridScaleType == null) { + if (collapseSmallUnconformities && hybridScale == null) { // Collapse small unconformities in pixel height space // TODO: this doesn't seem to work properly for non-age columns? let threshold = unconformityHeight ?? 30; diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index f0d6a9f3..1c3893b7 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -109,7 +109,9 @@ export const HeightScale: Story = { id: 448, // Ordered time bins axisType: ColumnAxisType.AGE, - hybridScaleType: HybridScaleType.ApproximateHeight, + hybridScale: { + type: HybridScaleType.ApproximateHeight, + }, showLabels: false, unitComponent: ColoredUnitComponent, showTimescale: true, @@ -124,7 +126,9 @@ export const KentuckyOrdovician: Story = { id: 448, // Ordered time bins axisType: ColumnAxisType.AGE, - hybridScaleType: HybridScaleType.ApproximateHeight, + hybridScale: { + type: HybridScaleType.ApproximateHeight, + }, showLabels: false, unitComponent: ColoredUnitComponent, showTimescale: true, @@ -140,7 +144,9 @@ export const WithApproximateHeightScale: Story = { id: 448, // Ordered time bins axisType: ColumnAxisType.AGE, - hybridScaleType: HybridScaleType.ApproximateHeight, + hybridScale: { + type: HybridScaleType.ApproximateHeight, + }, showLabels: false, unitComponent: ColoredUnitComponent, showTimescale: true, @@ -161,7 +167,9 @@ export const WithApproximateHeightScaleOnly: Story = { // Ordered time bins pixelScale: 0.5, axisType: ColumnAxisType.AGE, - hybridScaleType: HybridScaleType.ApproximateHeight, + hybridScale: { + type: HybridScaleType.ApproximateHeight, + }, showLabels: false, unitComponent: ColoredUnitComponent, showUnitPopover: true, From ba36617fda7e10fc5e62f73a493b65499bf659c3 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Thu, 27 Nov 2025 13:08:22 -0600 Subject: [PATCH 33/46] Fixed nonlinear scale --- packages/column-components/src/axis.ts | 12 ++++++- packages/column-views/src/age-axis.ts | 1 - packages/column-views/src/column.ts | 31 ++++++++++++++----- .../src/prepare-units/composite-scale.ts | 22 +++---------- .../column-views/src/prepare-units/index.ts | 10 ------ .../stories/nonlinear-scale.stories.ts | 5 +-- 6 files changed, 42 insertions(+), 39 deletions(-) diff --git a/packages/column-components/src/axis.ts b/packages/column-components/src/axis.ts index 918a48c3..886605e6 100644 --- a/packages/column-components/src/axis.ts +++ b/packages/column-components/src/axis.ts @@ -22,6 +22,7 @@ interface ColumnAxisProps { interface AgeAxisProps extends ColumnAxisProps { scale?: ScaleContinuousNumeric; + minTickSpacing?: number; } const __d3axisKeys = [ @@ -46,6 +47,7 @@ export function AgeAxis(props: AgeAxisProps) { className, showDomain = true, tickSpacing = 60, + minTickSpacing = 20, scale, } = props; @@ -55,6 +57,7 @@ export function AgeAxis(props: AgeAxisProps) { let tickValues: number[] = undefined; + let ticks = Math.max(Math.round(pixelHeight / tickSpacing), 2); if (pixelHeight < 3 * tickSpacing || scale.ticks(2).length < 2) { // Push ticks towards extrema const t0 = scale.ticks(4); @@ -62,8 +65,15 @@ export function AgeAxis(props: AgeAxisProps) { tickValues = [t0[0], t0[t0.length - 1]]; } + if (pixelHeight < minTickSpacing) { + ticks = 1; + tickValues = scale.ticks(1); + // Get the last tick value only + tickValues = [tickValues[0]]; + } + const defaultProps = { - ticks: Math.max(Math.round(pixelHeight / tickSpacing), 2), + ticks, // Suppress domain endpoints tickSizeOuter: 0, tickValues, diff --git a/packages/column-views/src/age-axis.ts b/packages/column-views/src/age-axis.ts index ad6ecec4..2dfe0b31 100644 --- a/packages/column-views/src/age-axis.ts +++ b/packages/column-views/src/age-axis.ts @@ -70,7 +70,6 @@ export function ApproximateHeightAxis(rest: CompositeAgeAxisProps) { const packages = sections.map((section) => { const { scaleInfo } = section; - console.log(scaleInfo); if (scaleInfo.heightScale == null) { throw new Error("No height scale available for section"); } diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 4b65473c..6458b7f3 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -13,6 +13,7 @@ import { HTMLAttributes, useCallback, CSSProperties, + ComponentType, } from "react"; import styles from "./column.module.sass"; import { @@ -38,12 +39,13 @@ import { CompositeTimescale, SectionsColumn, } from "./section"; -import { CompositeAgeAxis } from "./age-axis"; +import { ApproximateHeightAxis, CompositeAgeAxis } from "./age-axis"; import { MergeSectionsMode, usePreparedColumnUnits } from "./prepare-units"; import { UnitLong } from "@macrostrat/api-types"; import { NonIdealState } from "@blueprintjs/core"; import { DataField } from "@macrostrat/data-components"; import { ScaleContinuousNumeric } from "d3-scale"; +import { HybridScaleType } from "./prepare-units/dynamic-scales"; const h = hyperStyled(styles); @@ -151,11 +153,24 @@ export function Column(props: ColumnProps) { ); } - let main: any = h(ColumnInner, { columnRef: ref, ...rest }, [ - children, - h.if(showUnitPopover)(UnitSelectionPopover), - h.if(keyboardNavigation)(UnitKeyboardNavigation, { units }), - ]); + let ageAxisComponent = CompositeAgeAxis; + if ( + hybridScale?.type === HybridScaleType.ApproximateHeight && + axisType != ColumnAxisType.AGE + ) { + // Use approximate height axis for non-age columns if a non-age axis type is requested + ageAxisComponent = ApproximateHeightAxis; + } + + let main: any = h( + ColumnInner, + { columnRef: ref, ageAxisComponent, ...rest }, + [ + children, + h.if(showUnitPopover)(UnitSelectionPopover), + h.if(keyboardNavigation)(UnitKeyboardNavigation, { units }), + ], + ); /* By default, unit selection is disabled. However, if any related props are passed, we enable it. @@ -187,6 +202,7 @@ export function Column(props: ColumnProps) { interface ColumnInnerProps extends BaseColumnProps { columnRef: RefObject; + ageAxisComponent?: ComponentType; } function ColumnInner(props: ColumnInnerProps) { @@ -215,6 +231,7 @@ function ColumnInner(props: ColumnInnerProps) { timescaleLevels, maxInternalColumns, onMouseOver, + ageAxisComponent = CompositeAgeAxis, } = props; const { axisType } = useMacrostratColumnData(); @@ -243,7 +260,7 @@ function ColumnInner(props: ColumnInnerProps) { className, }, h("div.column", { ref: columnRef }, [ - h(CompositeAgeAxis), + h(ageAxisComponent), h.if(_showTimescale)(CompositeTimescale, { levels: timescaleLevels }), h(SectionsColumn, { unitComponent, diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 5c876648..5319d743 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -124,7 +124,6 @@ export function buildCompositeScaleInfo( export function finalizeSectionHeights( sections: SectionInfoWithScale[], unconformityHeight: number, - axisType: ColumnAxisType, ): CompositeColumnData { /** Finalize the heights of sections, including the heights of unconformities * between them. @@ -342,6 +341,7 @@ export interface CompositeColumnScale { copy(): CompositeColumnScale; domain(): [number, number]; invert(pixelHeight: number): number | null; + clamp(clamp: boolean): void; } export function createCompositeScale( @@ -349,18 +349,6 @@ export function createCompositeScale( interpolateUnconformities: boolean = false, ): CompositeColumnScale { /** Create a scale that works across multiple packages */ - // Get surfaces at which scale breaks - let scaleBreaks: [number, number, any][] = []; - for (const section of sections) { - const { pixelHeight, offset, domain, scale } = section.scaleInfo; - - console.log("Section", domain, offset); - - scaleBreaks.push([domain[1], offset, scale]); - scaleBreaks.push([domain[0], offset + pixelHeight, scale]); - } - // Sort the scale breaks by age - scaleBreaks.sort((a, b) => a[0] - b[0]); const scales: ScaleContinuousNumeric[] = []; @@ -384,7 +372,7 @@ export function createCompositeScale( lastScale = _scale; } - const scale = (age) => { + const scale: CompositeColumnScale = (age) => { for (const s of scales) { const domain = s.domain(); if (age >= domain[0] && age <= domain[domain.length - 1]) { @@ -430,10 +418,9 @@ export function createCompositeScale( for (const s of scales) { s.clamp(clamp); } - return scale; }; - return scale as CompositeColumnScale; + return scale; } /** Collapse sections separated by unconformities that are smaller than a given pixel height. */ @@ -495,8 +482,7 @@ export function collapseUnconformitiesByPixelHeight( b_pos, }; - const compositeSection = addScaleToSection(compositeSection0, opts); - currentSection = compositeSection; + currentSection = addScaleToSection(compositeSection0, opts); } else { // We need to keep the section newSections.push(currentSection); diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 27eed2b8..18081c80 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -56,15 +56,6 @@ export function prepareColumnUnits( scale, } = options; - /* Ordinal positioning does not really make sense for columns with overlapping - units, and there is little value to using API-provided ordinal positions anyway, - as they tend to be arbitrary. Therefore, we ignore t_pos and b_pos values for these - columns and compute an ordinal positioning of _surfaces_ within sections instead. - */ - // if (hybridScaleType == HybridScaleType.EquidistantSurfaces) { - // return prepareColumnUnitsEquidistant(units, options); - // } - if (scale != null) { // Set t_age and b_age based on scale domain if not already set const domain = scale.domain(); @@ -178,7 +169,6 @@ export function prepareColumnUnits( let { totalHeight, sections: sections2 } = finalizeSectionHeights( sectionsWithScales, unconformityHeight, - axisType, ); /** For each section, find units that are overlapping. diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index 1c3893b7..7924e265 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -5,7 +5,6 @@ import { Meta, StoryObj } from "@storybook/react-vite"; import { ApproximateHeightAxis, ColoredUnitComponent, - CompositeAgeAxis, ComputedSurfacesOverlay, MergeSectionsMode, } from "@macrostrat/column-views"; @@ -94,7 +93,9 @@ export const EquidistantSurfaces: Story = { id: 432, // Ordered time bins axisType: ColumnAxisType.AGE, - hybridScaleType: HybridScaleType.EquidistantSurfaces, + hybridScale: { + type: HybridScaleType.EquidistantSurfaces, + }, showLabels: false, unitComponent: ColoredUnitComponent, showTimescale: true, From 16f323d268966af096bb5be9e128bbf05203d318 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 00:53:26 -0600 Subject: [PATCH 34/46] Start adjusting to reduce interdependency --- .../src/field-locations/rockd-checkins/index.ts | 1 - packages/map-interface/src/location-info/index.ts | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/data-components/src/field-locations/rockd-checkins/index.ts b/packages/data-components/src/field-locations/rockd-checkins/index.ts index 3655dc26..b16986ed 100644 --- a/packages/data-components/src/field-locations/rockd-checkins/index.ts +++ b/packages/data-components/src/field-locations/rockd-checkins/index.ts @@ -6,7 +6,6 @@ import hyper from "@macrostrat/hyper"; import styles from "./index.module.sass"; import { LngLatCoords } from "@macrostrat/map-interface"; -import { useDarkMode } from "@macrostrat/ui-components"; import { Icon } from "@blueprintjs/core"; import mapboxgl from "mapbox-gl"; diff --git a/packages/map-interface/src/location-info/index.ts b/packages/map-interface/src/location-info/index.ts index 0d727481..08032783 100644 --- a/packages/map-interface/src/location-info/index.ts +++ b/packages/map-interface/src/location-info/index.ts @@ -5,7 +5,6 @@ import { normalizeLng, } from "@macrostrat/mapbox-utils"; import { formatValue } from "./utils"; -import { LngLat } from "mapbox-gl"; export * from "./hash-string"; @@ -50,9 +49,10 @@ export function LngLatCoords(props: LngLatProps) { let lat: number; let lng: number; - if (Array.isArray(position)) { + if (Array.isArray(position) && position.length === 2) { [lng, lat] = position; - } else if (position instanceof LngLat) { + } else if ("toArray" in position && typeof position.toArray === "function") { + // Check for LngLat object without access to mapbox-gl [lng, lat] = position.toArray(); } else if ("lng" in position) { lat = position.lat; From 94685b55c0223ddc2c6c518818396763d4f4dc20 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 01:08:55 -0600 Subject: [PATCH 35/46] Removed duplicate code from static-map-utils --- .../src/{location-info => }/hash-string.ts | 0 packages/map-interface/src/helpers.ts | 2 - packages/map-interface/src/index.ts | 1 + .../map-interface/src/location-info/index.ts | 2 - packages/static-map-utils/src/index.ts | 1 - .../static-map-utils/src/style/helpers.ts | 1 - packages/static-map-utils/src/style/index.ts | 2 +- packages/static-map-utils/src/tiled-map.ts | 1 - .../static-map-utils/src/transform-request.ts | 5 + packages/static-map-utils/src/utils.ts | 107 ------------------ 10 files changed, 7 insertions(+), 115 deletions(-) rename packages/map-interface/src/{location-info => }/hash-string.ts (100%) delete mode 100644 packages/static-map-utils/src/utils.ts diff --git a/packages/map-interface/src/location-info/hash-string.ts b/packages/map-interface/src/hash-string.ts similarity index 100% rename from packages/map-interface/src/location-info/hash-string.ts rename to packages/map-interface/src/hash-string.ts diff --git a/packages/map-interface/src/helpers.ts b/packages/map-interface/src/helpers.ts index accd861d..d831147d 100644 --- a/packages/map-interface/src/helpers.ts +++ b/packages/map-interface/src/helpers.ts @@ -2,7 +2,6 @@ import { useMapRef, useMapEaseTo, useMapDispatch, - useMapStatus, useMapInitialized, } from "@macrostrat/mapbox-react"; import { useMemo, useRef } from "react"; @@ -10,7 +9,6 @@ import { debounce } from "underscore"; import useResizeObserver from "use-resize-observer"; import { getMapPosition } from "@macrostrat/mapbox-utils"; -import mapboxgl from "mapbox-gl"; import { useCallback, useEffect, useState } from "react"; import { getMapPadding, useMapMarker } from "./utils"; import { useInDarkMode } from "@macrostrat/ui-components"; diff --git a/packages/map-interface/src/index.ts b/packages/map-interface/src/index.ts index c8046c2c..9f067fb7 100644 --- a/packages/map-interface/src/index.ts +++ b/packages/map-interface/src/index.ts @@ -9,3 +9,4 @@ export * from "./utils"; export * from "./location-info"; export * from "./expansion-panel"; export * from "./location-details"; +export * from "./hash-string"; diff --git a/packages/map-interface/src/location-info/index.ts b/packages/map-interface/src/location-info/index.ts index 08032783..e96bac50 100644 --- a/packages/map-interface/src/location-info/index.ts +++ b/packages/map-interface/src/location-info/index.ts @@ -6,8 +6,6 @@ import { } from "@macrostrat/mapbox-utils"; import { formatValue } from "./utils"; -export * from "./hash-string"; - export function ValueWithUnit(props) { const { value, unit } = props; return h("span.value-with-unit", [ diff --git a/packages/static-map-utils/src/index.ts b/packages/static-map-utils/src/index.ts index 9b33023c..38627888 100644 --- a/packages/static-map-utils/src/index.ts +++ b/packages/static-map-utils/src/index.ts @@ -1,4 +1,3 @@ -export * from "./utils"; export * from "./tiled-map"; export * from "./map-scale"; export * from "./transform-request"; diff --git a/packages/static-map-utils/src/style/helpers.ts b/packages/static-map-utils/src/style/helpers.ts index cf429f65..37670cd3 100644 --- a/packages/static-map-utils/src/style/helpers.ts +++ b/packages/static-map-utils/src/style/helpers.ts @@ -1,5 +1,4 @@ import type { StyleSpecification } from "maplibre-gl"; -import type mapboxgl from "mapbox-gl"; export function optimizeTerrain( style: StyleSpecification | null, diff --git a/packages/static-map-utils/src/style/index.ts b/packages/static-map-utils/src/style/index.ts index afb3f546..bf66409e 100644 --- a/packages/static-map-utils/src/style/index.ts +++ b/packages/static-map-utils/src/style/index.ts @@ -44,7 +44,7 @@ export function useInsetMapStyle(mapboxToken) { optimizeTerrain(baseStyle, "mapbox://mapbox.mapbox-terrain-dem-v1", [ "#ffffff", "#aaaaaa", - ]), + ]) as StyleSpecification, ); return style; }, [baseStyle]); diff --git a/packages/static-map-utils/src/tiled-map.ts b/packages/static-map-utils/src/tiled-map.ts index 59013791..2f68c4d8 100644 --- a/packages/static-map-utils/src/tiled-map.ts +++ b/packages/static-map-utils/src/tiled-map.ts @@ -372,7 +372,6 @@ export function getLineOverallAngle( const p2 = mercator.forward(coords[coords.length - 1]); const height = p2[1] - p1[1]; const width = p2[0] - p1[0]; - console.log(width, height); const angle = Math.atan2(height, width); return (angle * 180) / Math.PI; } diff --git a/packages/static-map-utils/src/transform-request.ts b/packages/static-map-utils/src/transform-request.ts index 98e0692f..7d499015 100644 --- a/packages/static-map-utils/src/transform-request.ts +++ b/packages/static-map-utils/src/transform-request.ts @@ -6,6 +6,11 @@ import { const SKU_ID = "01"; +interface SkuTokenObject { + token: string; + tokenExpiresAt: number; +} + function createSkuToken(): SkuTokenObject { // SKU_ID and TOKEN_VERSION are specified by an internal schema and should not change const TOKEN_VERSION = "1"; diff --git a/packages/static-map-utils/src/utils.ts b/packages/static-map-utils/src/utils.ts deleted file mode 100644 index 1c4dde05..00000000 --- a/packages/static-map-utils/src/utils.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { - useMapDispatch, - useMapInitialized, - useMapRef, - useMapStatus, -} from "@macrostrat/mapbox-react"; -import { useCallback, useEffect } from "react"; -import maplibre from "maplibre-gl"; -import { CameraPosition, MapPosition } from "@macrostrat/mapbox-utils"; -import { debounce } from "underscore"; - -export function StyleLoadedReporter({ onStyleLoaded = null }) { - /** Check back every 0.1 seconds to see if the map has loaded. - * We do it this way because mapboxgl loading events are unreliable */ - const isStyleLoaded = useMapStatus((state) => state.isStyleLoaded); - const mapRef = useMapRef(); - const dispatch = useMapDispatch(); - - useEffect(() => { - if (isStyleLoaded) return; - const interval = setInterval(() => { - const map = mapRef.current; - if (map == null) return; - if (map.isStyleLoaded()) { - // Wait a tick before setting the style loaded state - dispatch({ type: "set-style-loaded", payload: true }); - onStyleLoaded?.(map); - clearInterval(interval); - } - }, 50); - return () => clearInterval(interval); - }, [isStyleLoaded]); - - return null; -} - -/** Todo: reintegrate these utility functions with Mapbox utils */ -export function MapMovedReporter({ onMapMoved = null }) { - const mapRef = useMapRef(); - const dispatch = useMapDispatch(); - const isInitialized = useMapInitialized(); - - const mapMovedCallback = useCallback(() => { - const map = mapRef.current; - if (map == null) return; - const mapPosition = getMapPosition(map); - dispatch({ type: "map-moved", payload: mapPosition }); - onMapMoved?.(mapPosition, map); - }, [onMapMoved, dispatch, isInitialized]); - - useEffect(() => { - // Get the current value of the map. Useful for gradually moving away - // from class component - const map = mapRef.current; - if (map == null) return; - // Update the URI when the map moves - mapMovedCallback(); - const cb = debounce(mapMovedCallback, 100); - map.on("moveend", cb); - return () => { - map?.off("moveend", cb); - }; - }, [mapMovedCallback]); - return null; -} - -function getMapPosition(map: maplibre.Map): MapPosition { - return { - camera: getCameraPosition(map), - target: { - ...map.getCenter(), - zoom: map.getZoom(), - }, - }; -} - -function getCameraPosition(map: maplibre.Map): CameraPosition { - const latLong = map.transform.getCameraLngLat(); - return { - lng: latLong.lng, - lat: latLong.lat, - altitude: map.transform.getCameraAltitude(), - pitch: map.getPitch(), - bearing: map.getBearing(), - }; -} - -export function setMapPosition(map: maplibre.Map, pos: MapPosition) { - const { pitch = 0, bearing = 0, altitude } = pos.camera; - const zoom = pos.target?.zoom; - if (zoom != null && altitude == null && pitch == 0 && bearing == 0) { - const { lng, lat } = pos.target; - // Zoom must be set before center to correctly recall position - map.setZoom(zoom); - map.setCenter([lng, lat]); - } else { - const { altitude, lng, lat } = pos.camera; - map.jumpTo({ - center: [lng, lat], - zoom: zoom ?? map.getZoom(), - bearing: bearing ?? map.getBearing(), - pitch: pitch ?? map.getPitch(), - // @ts-ignore - altitude: altitude, - }); - } -} From 3832de9c9d4cbf2388e7b0003eb3365086737451 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 01:21:17 -0600 Subject: [PATCH 36/46] Move location info to data-components Fixes #158 --- packages/data-components/package.json | 2 +- .../src/field-locations/rockd-checkins/index.ts | 2 +- packages/data-components/src/index.ts | 1 + .../src/location-info/index.ts | 3 ++- .../src/location-info/location-info.stories.ts} | 5 ++--- .../src/location-info/utils.ts | 0 packages/map-interface/src/hash-string.ts | 5 ++++- packages/map-interface/src/index.ts | 1 - packages/map-interface/src/location-panel/header.ts | 2 +- packages/ui-components/stories/model-editor.stories.ts | 4 +--- yarn.lock | 2 +- 11 files changed, 14 insertions(+), 13 deletions(-) rename packages/{map-interface => data-components}/src/location-info/index.ts (97%) rename packages/{map-interface/stories/coordinates.stories.ts => data-components/src/location-info/location-info.stories.ts} (83%) rename packages/{map-interface => data-components}/src/location-info/utils.ts (100%) diff --git a/packages/data-components/package.json b/packages/data-components/package.json index 8428d363..6e4e61db 100644 --- a/packages/data-components/package.json +++ b/packages/data-components/package.json @@ -43,7 +43,7 @@ "dependencies": { "@macrostrat/color-utils": "workspace:^", "@macrostrat/hyper": "^3.0.6", - "@macrostrat/map-interface": "workspace:^", + "@macrostrat/mapbox-utils": "workspace:^", "@macrostrat/stratigraphy-utils": "workspace:^", "@macrostrat/ui-components": "workspace:^", "@visx/axis": "^3.12.0", diff --git a/packages/data-components/src/field-locations/rockd-checkins/index.ts b/packages/data-components/src/field-locations/rockd-checkins/index.ts index b16986ed..479f09da 100644 --- a/packages/data-components/src/field-locations/rockd-checkins/index.ts +++ b/packages/data-components/src/field-locations/rockd-checkins/index.ts @@ -5,7 +5,7 @@ import hyper from "@macrostrat/hyper"; import styles from "./index.module.sass"; -import { LngLatCoords } from "@macrostrat/map-interface"; +import { LngLatCoords } from "../../location-info"; import { Icon } from "@blueprintjs/core"; import mapboxgl from "mapbox-gl"; diff --git a/packages/data-components/src/index.ts b/packages/data-components/src/index.ts index efb4258b..2f76699c 100644 --- a/packages/data-components/src/index.ts +++ b/packages/data-components/src/index.ts @@ -1,3 +1,4 @@ export * from "./components"; export * from "./dz-spectrum"; export * from "./field-locations"; +export * from "./location-info"; diff --git a/packages/map-interface/src/location-info/index.ts b/packages/data-components/src/location-info/index.ts similarity index 97% rename from packages/map-interface/src/location-info/index.ts rename to packages/data-components/src/location-info/index.ts index e96bac50..3903bad7 100644 --- a/packages/map-interface/src/location-info/index.ts +++ b/packages/data-components/src/location-info/index.ts @@ -5,6 +5,7 @@ import { normalizeLng, } from "@macrostrat/mapbox-utils"; import { formatValue } from "./utils"; +import type { LngLatLike } from "mapbox-gl"; export function ValueWithUnit(props) { const { value, unit } = props; @@ -27,7 +28,7 @@ export function DegreeCoord(props) { export interface LngLatProps { /** Map position */ - position: mapboxgl.LngLatLike | null; + position: LngLatLike | null; className?: string; /** Zoom level (used to infer coordinate rounding if provided) */ zoom?: number; diff --git a/packages/map-interface/stories/coordinates.stories.ts b/packages/data-components/src/location-info/location-info.stories.ts similarity index 83% rename from packages/map-interface/stories/coordinates.stories.ts rename to packages/data-components/src/location-info/location-info.stories.ts index 6fa07820..491cf11e 100644 --- a/packages/map-interface/stories/coordinates.stories.ts +++ b/packages/data-components/src/location-info/location-info.stories.ts @@ -1,11 +1,10 @@ -import h from "@macrostrat/hyper"; import type { Meta, StoryObj } from "@storybook/react-vite"; -import { LngLatCoords, LngLatProps } from "../src/location-info"; +import { LngLatCoords, LngLatProps } from "."; // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export const meta: Meta = { - title: "Map interface/Utilities/LngLatCoords", + title: "Data components/Location info", component: LngLatCoords, }; diff --git a/packages/map-interface/src/location-info/utils.ts b/packages/data-components/src/location-info/utils.ts similarity index 100% rename from packages/map-interface/src/location-info/utils.ts rename to packages/data-components/src/location-info/utils.ts diff --git a/packages/map-interface/src/hash-string.ts b/packages/map-interface/src/hash-string.ts index e08cc7fc..1e2a3b9a 100644 --- a/packages/map-interface/src/hash-string.ts +++ b/packages/map-interface/src/hash-string.ts @@ -4,7 +4,6 @@ import { formatCoordForZoomLevel, } from "@macrostrat/mapbox-utils"; import { ParsedQuery } from "query-string"; -import { fmt1, fmt2, fmtInt } from "./utils"; interface LocationHashParams { x?: string; @@ -14,6 +13,10 @@ interface LocationHashParams { e?: string; } +const fmt1 = (x: number) => x.toFixed(1); +const fmt2 = (x: number) => x.toFixed(2); +const fmtInt = (x: number) => Math.round(x).toString(); + export function applyMapPositionToHash( args: LocationHashParams, mapPosition: MapPosition | null, diff --git a/packages/map-interface/src/index.ts b/packages/map-interface/src/index.ts index 9f067fb7..92a2c61d 100644 --- a/packages/map-interface/src/index.ts +++ b/packages/map-interface/src/index.ts @@ -6,7 +6,6 @@ export * from "./map-view"; export * from "./controls"; export * from "./helpers"; export * from "./utils"; -export * from "./location-info"; export * from "./expansion-panel"; export * from "./location-details"; export * from "./hash-string"; diff --git a/packages/map-interface/src/location-panel/header.ts b/packages/map-interface/src/location-panel/header.ts index e774ab11..4eca3d19 100644 --- a/packages/map-interface/src/location-panel/header.ts +++ b/packages/map-interface/src/location-panel/header.ts @@ -2,7 +2,7 @@ import { Icon, Button } from "@blueprintjs/core"; import hyper from "@macrostrat/hyper"; import styles from "./main.module.sass"; import { useToaster } from "@macrostrat/ui-components"; -import { LngLatCoords, Elevation } from "../location-info"; +import { LngLatCoords, Elevation } from "@macrostrat/data-components"; import { LocationFocusButton, useFocusState } from "@macrostrat/mapbox-react"; import classNames from "classnames"; import type { ReactNode } from "react"; diff --git a/packages/ui-components/stories/model-editor.stories.ts b/packages/ui-components/stories/model-editor.stories.ts index ef19bb04..b357a792 100644 --- a/packages/ui-components/stories/model-editor.stories.ts +++ b/packages/ui-components/stories/model-editor.stories.ts @@ -8,10 +8,8 @@ import { import h from "@macrostrat/hyper"; import "./stories.sass"; -import { LngLatCoords, LngLatProps } from "packages/map-interface/src"; - // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export -const meta: Meta = { +const meta: Meta = { title: "UI components/Model Editor", component: ModelEditorExample, }; diff --git a/yarn.lock b/yarn.lock index bdc2cbb6..78cd618e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2373,7 +2373,7 @@ __metadata: "@blueprintjs/core": "npm:^5.10.2" "@macrostrat/color-utils": "workspace:^" "@macrostrat/hyper": "npm:^3.0.6" - "@macrostrat/map-interface": "workspace:^" + "@macrostrat/mapbox-utils": "workspace:^" "@macrostrat/stratigraphy-utils": "workspace:^" "@macrostrat/ui-components": "workspace:^" "@types/d3-array": "npm:^3.2.1" From 2f816dad595e3d1c50e8f5bb041ec0d14d33c913 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 02:02:25 -0600 Subject: [PATCH 37/46] Updated unit details panel --- .../src/unit-details/panel.module.sass | 2 +- .../column-views/src/unit-details/panel.ts | 55 ++++++++++--------- .../stories/unit-details.stories.ts | 17 +++++- packages/mapbox-react/src/hooks.ts | 1 - 4 files changed, 46 insertions(+), 29 deletions(-) diff --git a/packages/column-views/src/unit-details/panel.module.sass b/packages/column-views/src/unit-details/panel.module.sass index 3ce404a5..54ecb227 100644 --- a/packages/column-views/src/unit-details/panel.module.sass +++ b/packages/column-views/src/unit-details/panel.module.sass @@ -13,7 +13,7 @@ overflow: hidden .title-container gap: 0.5em - align-items: center + align-items: baseline margin: 0 flex-grow: 1 z-index: 5 diff --git a/packages/column-views/src/unit-details/panel.ts b/packages/column-views/src/unit-details/panel.ts index 2988fa92..3c6dfd5d 100644 --- a/packages/column-views/src/unit-details/panel.ts +++ b/packages/column-views/src/unit-details/panel.ts @@ -40,7 +40,8 @@ export function UnitDetailsPanel({ ]), lithologyFeatures, actions, - selectUnit, + hiddenActions = null, + onSelectUnit, columnUnits, onClickItem, }: { @@ -49,10 +50,11 @@ export function UnitDetailsPanel({ showLithologyProportions?: boolean; className?: string; actions?: ReactNode; + hiddenActions?: ReactNode; features?: Set; lithologyFeatures?: Set; columnUnits?: UnitLong[]; - selectUnit?: (unitID: number) => void; + onSelectUnit?: (unitID: number) => void; onClickItem?: (item: any) => void; }) { const [showJSON, setShowJSON] = useState(false); @@ -66,24 +68,27 @@ export function UnitDetailsPanel({ features, lithologyFeatures, onClickItem, + onSelectUnit, }); } let title = defaultNameFunction(unit); - let hiddenActions = null; if (features.has(UnitDetailsFeature.JSONToggle)) { - hiddenActions = h(Button, { - icon: "code", - small: true, - minimal: true, - key: "json-view-toggle", - className: classNames("json-view-toggle", { enabled: setShowJSON }), - onClick(evt) { - setShowJSON(!showJSON); - evt.stopPropagation(); - }, - }); + hiddenActions = h([ + h(Button, { + icon: "code", + small: true, + minimal: true, + key: "json-view-toggle", + className: classNames("json-view-toggle", { enabled: setShowJSON }), + onClick(evt) { + setShowJSON(!showJSON); + evt.stopPropagation(); + }, + }), + hiddenActions, + ]); } return h("div.unit-details-panel", { className }, [ @@ -114,15 +119,13 @@ export function LegendPanelHeader({ return h("header.legend-panel-header", [ h("div.title-container", [ h.if(title != null)("h3", title), - h.if(hiddenActions != null || id != null)( + h.if(hiddenActions != null)( "span.hidden-actions-container", - h("div.hidden-actions", [ - h.if(id != null)("code.unit-id", id), - h.if(hiddenActions != null)([hiddenActions]), - ]), + h("div.hidden-actions", hiddenActions), ), ]), h("div.spacer"), + h.if(id != null)("code.unit-id", id), h.if(actions != null)(ButtonGroup, { minimal: true }, actions), h.if(onClose != null)(Button, { icon: "cross", @@ -145,7 +148,7 @@ export enum UnitDetailsFeature { function UnitDetailsContent({ unit, - selectUnit, + onSelectUnit, columnUnits, lithologyFeatures = new Set([ LithologyTagFeature.Proportion, @@ -159,7 +162,7 @@ function UnitDetailsContent({ getItemHref, }: { unit: UnitLong; - selectUnit?: (unitID: number) => void; + onSelectUnit?: (unitID: number) => void; columnUnits?: UnitLong[]; lithologyFeatures?: Set; features?: Set; @@ -269,12 +272,12 @@ function UnitDetailsContent({ h( DataField, { label: "Above" }, - h(UnitIDList, { units: unit.units_above, selectUnit }), + h(UnitIDList, { units: unit.units_above, onSelectUnit }), ), h( DataField, { label: "Below" }, - h(UnitIDList, { units: unit.units_below, selectUnit }), + h(UnitIDList, { units: unit.units_below, onSelectUnit }), ), ]), colorSwatch, @@ -556,7 +559,7 @@ export function Identifier({ ); } -function UnitIDList({ units, selectUnit }) { +function UnitIDList({ units, onSelectUnit }) { const u1 = units.filter((d) => d != 0); if (u1.length === 0) { @@ -564,7 +567,7 @@ function UnitIDList({ units, selectUnit }) { } let tag = "span"; - if (selectUnit != null) { + if (onSelectUnit != null) { tag = "a"; } @@ -575,7 +578,7 @@ function UnitIDList({ units, selectUnit }) { return h(Identifier, { className: "unit-id", onClick() { - selectUnit?.(unitID); + onSelectUnit?.(unitID); }, key: unitID, id: unitID, diff --git a/packages/column-views/stories/unit-details.stories.ts b/packages/column-views/stories/unit-details.stories.ts index a5f9831f..3792a02f 100644 --- a/packages/column-views/stories/unit-details.stories.ts +++ b/packages/column-views/stories/unit-details.stories.ts @@ -2,7 +2,7 @@ import h from "@macrostrat/hyper"; import { Meta, StoryObj } from "@storybook/react-vite"; import { useAPIResult } from "@macrostrat/ui-components"; -import { Spinner } from "@blueprintjs/core"; +import { Button, Spinner } from "@blueprintjs/core"; import "@macrostrat/style-system"; import { UnitDetailsPanel } from "../src/unit-details"; import { LithologiesProvider } from "../src"; @@ -108,3 +108,18 @@ export const eODPMudstone: Story = { inProcess: true, }, }; + +export const WithActions: Story = { + args: { + unit_id: 62623, + onClose() { + console.log("Close"); + }, + actions: h([ + h(Button, { text: "Action 1", onClick: () => alert("Action 1") }), + ]), + hiddenActions: h([ + h(Button, { icon: "add-column-left", onClick: () => alert("Hidden") }), + ]), + }, +}; diff --git a/packages/mapbox-react/src/hooks.ts b/packages/mapbox-react/src/hooks.ts index 87841258..3900afef 100644 --- a/packages/mapbox-react/src/hooks.ts +++ b/packages/mapbox-react/src/hooks.ts @@ -1,5 +1,4 @@ import { RefObject, useEffect } from "react"; -import mapboxgl from "mapbox-gl"; import { toggleMapLabelVisibility } from "@macrostrat/mapbox-utils"; import { useMapRef, useMapStatus } from "./context"; import { useCallback } from "react"; From 044a851868f9f157c7a426dfaa16f0a576eac81d Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 02:45:11 -0600 Subject: [PATCH 38/46] Fixed unit popover navigation --- .../column-views/src/unit-details/popover.ts | 5 ++++ packages/column-views/src/units/selection.ts | 23 +++++++++++++++++-- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/packages/column-views/src/unit-details/popover.ts b/packages/column-views/src/unit-details/popover.ts index f27fa403..60c551c0 100644 --- a/packages/column-views/src/unit-details/popover.ts +++ b/packages/column-views/src/unit-details/popover.ts @@ -54,6 +54,7 @@ function InteractionBarrier({ children }) { export function UnitSelectionPopover() { const unit = useSelectedUnit(); + const selectUnit = useUnitSelectionStore((state) => state.onUnitSelected); const position = useUnitSelectionStore((state) => state.overlayPosition); if (unit == null) { return null; @@ -76,6 +77,10 @@ export function UnitSelectionPopover() { unit, showLithologyProportions: true, className: "legend-panel", + onSelectUnit: (id: number) => { + console.log("Selected unit in popover:", id); + selectUnit(id, null, null); + }, }), ), ); diff --git a/packages/column-views/src/units/selection.ts b/packages/column-views/src/units/selection.ts index e83da09b..f87f72d7 100644 --- a/packages/column-views/src/units/selection.ts +++ b/packages/column-views/src/units/selection.ts @@ -20,7 +20,7 @@ import { } from "@macrostrat/column-views"; type UnitSelectDispatch = ( - unit: BaseUnit | null, + unit: number | BaseUnit | null, target: HTMLElement | null, event: Event | null, ) => void; @@ -92,7 +92,26 @@ export function UnitSelectionProvider(props: { }); } }, - onUnitSelected: (unit: T, target: HTMLElement, event: PointerEvent) => { + onUnitSelected: ( + input: number | T | null, + target: HTMLElement = null, + event: PointerEvent = null, + ) => { + if (input == null) { + return set({ + selectedUnit: null, + selectedUnitData: null, + overlayPosition: null, + }); + } + + let unit: T | null = null; + if (typeof input === "number") { + unit = props.units.find((u) => u.unit_id === input) || null; + } else { + unit = input; + } + const el = props.columnRef?.current; let overlayPosition = null; From 3612ac182eda4978d7ba07359e161e3c16354a11 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 04:41:38 -0600 Subject: [PATCH 39/46] Improve unit names --- .../column-views/src/data-provider/store.ts | 12 ++ .../src/unit-details/panel.module.sass | 7 + .../column-views/src/unit-details/panel.ts | 133 +++++++++++++----- 3 files changed, 120 insertions(+), 32 deletions(-) diff --git a/packages/column-views/src/data-provider/store.ts b/packages/column-views/src/data-provider/store.ts index db298cf5..f28c1266 100644 --- a/packages/column-views/src/data-provider/store.ts +++ b/packages/column-views/src/data-provider/store.ts @@ -63,6 +63,18 @@ export function useMacrostratUnits() { return useMacrostratColumnData().units; } +export function useColumnUnitsMap(): Map | null { + const ctx = useContext(MacrostratColumnDataContext); + return useMemo(() => { + if (ctx == null) return null; + const unitMap = new Map(); + ctx.units.forEach((unit) => { + unitMap.set(unit.unit_id, unit); + }); + return unitMap; + }, [ctx?.units]); +} + export function useCompositeScale(): CompositeColumnScale { const ctx = useMacrostratColumnData(); return useMemo( diff --git a/packages/column-views/src/unit-details/panel.module.sass b/packages/column-views/src/unit-details/panel.module.sass index 54ecb227..d70d4eea 100644 --- a/packages/column-views/src/unit-details/panel.module.sass +++ b/packages/column-views/src/unit-details/panel.module.sass @@ -50,6 +50,13 @@ position: relative margin: 1em +.units-list + .item + &:after + content: ', ' + &:last-child:after + content: '' + .proportion, .sep, .no-units, .strat-name-text color: var(--secondary-color) font-style: italic diff --git a/packages/column-views/src/unit-details/panel.ts b/packages/column-views/src/unit-details/panel.ts index 3c6dfd5d..c0fc3831 100644 --- a/packages/column-views/src/unit-details/panel.ts +++ b/packages/column-views/src/unit-details/panel.ts @@ -14,7 +14,13 @@ import { Parenthetical, Value, } from "@macrostrat/data-components"; -import { useMacrostratData, useMacrostratDefs } from "../data-provider"; +import { + useColumnUnitsIfAvailable, + useColumnUnitsMap, + useMacrostratColumnData, + useMacrostratData, + useMacrostratDefs, +} from "../data-provider"; import { Environment, UnitLong, @@ -272,12 +278,20 @@ function UnitDetailsContent({ h( DataField, { label: "Above" }, - h(UnitIDList, { units: unit.units_above, onSelectUnit }), + h(UnitIDList, { + units: unit.units_above, + onSelectUnit, + showNames: true, + }), ), h( DataField, { label: "Below" }, - h(UnitIDList, { units: unit.units_below, onSelectUnit }), + h(UnitIDList, { + units: unit.units_below, + onSelectUnit, + showNames: true, + }), ), ]), colorSwatch, @@ -532,6 +546,20 @@ function enhanceLithologies( }); } +export function ClickableText({ + onClick, + className, + children, +}: { + onClick: () => void; + className?: string; + children: ReactNode; +}) { + /** An optionally clickable text element */ + const tag = onClick != null ? "a" : "span"; + return h(tag, { onClick, className }, children); +} + export function Identifier({ id, onClick, @@ -542,51 +570,92 @@ export function Identifier({ className?: string; }) { /** An item that displays a numeric identifier, optionally clickable */ - const tag = onClick != null ? "a" : "span"; + const _onClick = onClick != null ? () => onClick(id) : null; + return h( - tag, + ClickableText, { - onClick() { - onClick?.(id); - }, - className: classNames( - "identifier", - { clickable: onClick != null }, - className, - ), + onClick: _onClick, + className: classNames("identifier", className), }, id, ); } -function UnitIDList({ units, onSelectUnit }) { - const u1 = units.filter((d) => d != 0); +type UnitInfo = { + unitID: number; + colID?: number; + name?: string; +}; - if (u1.length === 0) { - return h("span.no-units", "None"); - } +function UnitIDList({ units, onSelectUnit, showNames = false }) { + const unitsMap = useColumnUnitsMap(); + + const extUnits: UnitInfo[] = useMemo(() => { + const u1 = units.filter((d) => d != 0); + if (showNames) { + return u1.map((unitID) => { + const unitData = unitsMap.get(unitID); + let name: string = undefined; + if (unitData != null) { + name = defaultNameFunction(unitData); + } + return { + unitID, + colID: unitData?.col_id, + name, + }; + }); + } else { + return u1.map((unitID) => ({ unitID })); + } + }, [units, unitsMap]); - let tag = "span"; - if (onSelectUnit != null) { - tag = "a"; + if (extUnits.length === 0) { + return h("span.no-units", "None"); } return h( ItemList, - { className: "unit-id-list" }, - u1.map((unitID) => { - return h(Identifier, { - className: "unit-id", - onClick() { - onSelectUnit?.(unitID); - }, - key: unitID, - id: unitID, - }); - }), + { className: "units-list" }, + extUnits.map((info) => + h("span.item", h(UnitIdentifier, { ...info, onSelectUnit })), + ), ); } +function UnitIdentifier({ + unitID, + colID, + name, + onSelectUnit, +}: UnitInfo & { onSelectUnit?: (unitID: number) => void }) { + const onClick = useMemo(() => { + if (onSelectUnit == null) return null; + return () => { + onSelectUnit(unitID); + }; + }, [onSelectUnit]); + + if (name != null) { + return h( + ClickableText, + { + className: "unit-name", + onClick, + }, + name, + ); + } + + return h(Identifier, { + className: "unit-id", + onClick, + key: unitID, + id: unitID, + }); +} + function IntervalProportions({ unit, onClickItem }) { const i0 = unit.b_int_id; const i1 = unit.t_int_id; From 2d4d62a5416fe54e84298ec489910a7c7365a89b Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 05:23:14 -0600 Subject: [PATCH 40/46] Got rid of more circular dependencies --- package.json | 2 +- packages/api-types/src/defs.d.ts | 4 +- packages/column-views/src/column.ts | 9 +- .../src/correlation-chart/prepare-data.ts | 2 +- .../column-views/src/data-provider/base.ts | 10 +- .../column-views/src/data-provider/fetch.ts | 13 +- .../column-views/src/data-provider/store.ts | 3 +- .../src/prepare-units/composite-scale.ts | 91 ++---------- .../src/prepare-units/dynamic-scales.ts | 46 ++---- .../column-views/src/prepare-units/helpers.ts | 28 +--- .../column-views/src/prepare-units/index.ts | 1 + .../column-views/src/prepare-units/types.ts | 135 ++++++++++++++++++ .../column-views/src/prepare-units/utils.ts | 10 +- packages/column-views/src/section.ts | 3 +- .../column-views/src/unit-details/panel.ts | 4 +- .../stories/nonlinear-scale.stories.ts | 2 +- .../src/style-image-manager/index.ts | 2 +- packages/timescale/src/index.ts | 32 ++--- packages/timescale/src/types.ts | 11 +- 19 files changed, 218 insertions(+), 190 deletions(-) create mode 100644 packages/column-views/src/prepare-units/types.ts diff --git a/package.json b/package.json index 03e387f2..d2221512 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "publish:storybook": "./scripts/publish-storybook.sh", "format": "prettier --write .", "check-types": "tsc --noEmit", - "check-circular": "madge --circular --extensions ts,tsx ." + "check-circular": "madge --exclude __archive --circular --extensions ts,tsx ." }, "author": "Daven Quinn", "license": "ISC", diff --git a/packages/api-types/src/defs.d.ts b/packages/api-types/src/defs.d.ts index 5f21081b..5509e0e9 100644 --- a/packages/api-types/src/defs.d.ts +++ b/packages/api-types/src/defs.d.ts @@ -39,4 +39,6 @@ export type MacrostratInterval = { color: string; }; -export type Interval = MacrostratInterval; +type Interval = MacrostratInterval; + +export type { Interval }; diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 6458b7f3..678813c5 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -23,7 +23,6 @@ import { useUnitSelectionDispatch, } from "./units"; -import { ColumnHeightScaleOptions } from "./prepare-units/composite-scale"; import { Identifier, ReferencesField, @@ -40,12 +39,16 @@ import { SectionsColumn, } from "./section"; import { ApproximateHeightAxis, CompositeAgeAxis } from "./age-axis"; -import { MergeSectionsMode, usePreparedColumnUnits } from "./prepare-units"; +import { + MergeSectionsMode, + usePreparedColumnUnits, + HybridScaleType, + ColumnHeightScaleOptions, +} from "./prepare-units"; import { UnitLong } from "@macrostrat/api-types"; import { NonIdealState } from "@blueprintjs/core"; import { DataField } from "@macrostrat/data-components"; import { ScaleContinuousNumeric } from "d3-scale"; -import { HybridScaleType } from "./prepare-units/dynamic-scales"; const h = hyperStyled(styles); diff --git a/packages/column-views/src/correlation-chart/prepare-data.ts b/packages/column-views/src/correlation-chart/prepare-data.ts index eda8142d..07a47c8a 100644 --- a/packages/column-views/src/correlation-chart/prepare-data.ts +++ b/packages/column-views/src/correlation-chart/prepare-data.ts @@ -1,5 +1,4 @@ import { CompositeStratigraphicScaleInfo } from "../age-axis"; -import { PackageLayoutData } from "../prepare-units/composite-scale"; import { ColumnAxisType } from "@macrostrat/column-components"; import { type ColumnGeoJSONRecord, UnitLong } from "@macrostrat/api-types"; import { @@ -9,6 +8,7 @@ import { preprocessUnits, } from "../prepare-units"; import { mergeAgeRanges } from "@macrostrat/stratigraphy-utils"; +import { PackageLayoutData } from "../prepare-units/types"; export interface ColumnIdentifier { col_id: number; diff --git a/packages/column-views/src/data-provider/base.ts b/packages/column-views/src/data-provider/base.ts index b448bc6f..2817864a 100644 --- a/packages/column-views/src/data-provider/base.ts +++ b/packages/column-views/src/data-provider/base.ts @@ -9,6 +9,7 @@ import { Environment, MacrostratRef, StratName, + Interval, } from "@macrostrat/api-types"; import { fetchAllColumns, @@ -49,7 +50,10 @@ interface MacrostratStore extends RefsSlice { lithologies: Map | null; getLithologies(ids: number[] | null): Promise; intervals: Map | null; - getIntervals(ids: number[] | null, timescaleID: number | null): Promise; + getIntervals( + ids: number[] | null, + timescaleID: number | null, + ): Promise; environments: Map | null; getEnvironments(ids: number[] | null): Promise; columnFootprints: Map; @@ -125,13 +129,13 @@ function createColumnsSlice(set, get) { let footprints = columnFootprints.get(key); if (footprints == null || footprints.inProcess != _inProcess) { // Fetch the columns - const statusCode = ["active"]; + const statusCode: ColumnStatusCode[] = ["active"]; if (_inProcess) { statusCode.push("in process"); } const columns = await fetchAllColumns({ projectID, - statusCode: statusCode.join(","), + statusCode, fetch, }); if (columns == null) { diff --git a/packages/column-views/src/data-provider/fetch.ts b/packages/column-views/src/data-provider/fetch.ts index 2919d6b8..23778691 100644 --- a/packages/column-views/src/data-provider/fetch.ts +++ b/packages/column-views/src/data-provider/fetch.ts @@ -14,6 +14,7 @@ import { import crossFetch from "cross-fetch"; import { feature } from "topojson-client"; import { geoArea } from "d3-geo"; +import _ from "underscore"; function defaultFetch( url: string, @@ -23,10 +24,12 @@ function defaultFetch( return crossFetch(baseURL + url, options); } +export type ColumnStatusCode = "in process" | "active" | "obsolete"; + export interface ColumnFetchOptions { apiBaseURL?: string; projectID?: number; - statusCode?: "in process"; + statusCode?: ColumnStatusCode | ColumnStatusCode[]; format?: "geojson" | "topojson" | "geojson_bare"; fetch?: any; } @@ -46,9 +49,13 @@ export async function fetchAllColumns( if (projectID != null) { args = { ...args, project_id: projectID }; } - if (statusCode != null) { - args = { ...args, status_code: statusCode }; + let _statusCode: string | undefined = undefined; + if (Array.isArray(statusCode)) { + _statusCode = statusCode.join(","); + } else if (statusCode != null) { + _statusCode = statusCode; } + args.statusCode = _statusCode; if (projectID == null) { args = { ...args, all: true }; diff --git a/packages/column-views/src/data-provider/store.ts b/packages/column-views/src/data-provider/store.ts index f28c1266..846d06bf 100644 --- a/packages/column-views/src/data-provider/store.ts +++ b/packages/column-views/src/data-provider/store.ts @@ -3,10 +3,9 @@ import h from "@macrostrat/hyper"; import { CompositeColumnScale, createCompositeScale, - PackageLayoutData, } from "../prepare-units/composite-scale"; -import { ExtUnit } from "../prepare-units/helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; +import type { ExtUnit, PackageLayoutData } from "../prepare-units"; export interface MacrostratColumnDataContext { units: ExtUnit[]; diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 5319d743..7ab538ac 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -1,90 +1,21 @@ -import type { ExtUnit, SectionInfo } from "./helpers"; import { ColumnAxisType } from "@macrostrat/column-components"; import { ensureArray, getUnitHeightRange } from "./utils"; import { ScaleContinuousNumeric, scaleLinear } from "d3-scale"; import { UnitLong } from "@macrostrat/api-types"; -import { - buildHybridScale, - HybridScaleDefinition, - HybridScaleType, -} from "./dynamic-scales"; - -export interface ColumnHeightScaleOptions { - /** A fixed pixel scale to use for the section (pixels per Myr) */ - pixelScale?: number; - /** The target height of a constituent unit in pixels, for dynamic - * scale generation */ - targetUnitHeight?: number; - /** Min height of a section in pixels. Will override minPixelScale in some cases. */ - minSectionHeight?: number; - /** The minimum pixel scale to use for the section (pixels per Myr). This is mostly - * needed because small sections (<1-2 units) don't necessarily have space to comfortably - * render two axis labels */ - minPixelScale?: number; - // Axis scale type - axisType?: ColumnAxisType; - // Unconformity height in pixels - unconformityHeight?: number; - // Whether to collapse unconformities that are less than a height threshold - collapseSmallUnconformities?: boolean | number; - // A continuous scale to use instead of generating one - // TODO: discontinuous scales are not yet supported - scale?: ScaleContinuousNumeric; - // Hybrid scale type. - // This overrides parameters such as the axis type - hybridScale?: HybridScaleDefinition; -} - -export interface SectionScaleOptions extends ColumnHeightScaleOptions { - axisType: ColumnAxisType; - domain: [number, number]; -} - -/** Output of a section scale. For now, this assumes that the - * mapping is linear, but it could be extended to support arbitrary - * scale functions. - */ -export interface PackageScaleInfo { - domain: [number, number]; - pixelHeight: number; - // TODO: add a function - scale: ScaleContinuousNumeric; - pixelScale?: number; // if it's a linear scale, this could be defined - // Subsidiary scale for height mapping (for hybrid scales) - heightScale?: ScaleContinuousNumeric; -} - -export type PackageScaleLayoutData = PackageScaleInfo & { - // A unique key for the section to use in React - key: string; - offset: number; - // How much to - paddingTop: number; -}; - -export type PackageLayoutData = SectionInfo & { - scaleInfo: PackageScaleLayoutData; - // A unique key for the section to use in React - key: string; -}; - -export interface CompositeScaleData { - totalHeight: number; - sections: PackageScaleLayoutData[]; -} - -export interface ColumnScaleOptions extends ColumnHeightScaleOptions { - axisType: ColumnAxisType; - unconformityHeight: number; -} +import { buildHybridScale } from "./dynamic-scales"; +import { ExtUnit, HybridScaleType, SectionInfo } from "./types"; +import type { + ColumnScaleOptions, + CompositeColumnData, + CompositeScaleData, + PackageLayoutData, + PackageScaleInfo, + PackageScaleLayoutData, + SectionScaleOptions, +} from "./types"; // Composite scale information augmented with units in each package -export interface CompositeColumnData - extends Omit { - sections: PackageLayoutData[]; -} - export function buildCompositeScaleInfo( inputScales: PackageScaleInfo[], unconformityHeight: number, diff --git a/packages/column-views/src/prepare-units/dynamic-scales.ts b/packages/column-views/src/prepare-units/dynamic-scales.ts index af86eb26..fcbad954 100644 --- a/packages/column-views/src/prepare-units/dynamic-scales.ts +++ b/packages/column-views/src/prepare-units/dynamic-scales.ts @@ -1,36 +1,16 @@ -import { ExtUnit } from "./helpers"; -import { UnitLong } from "@macrostrat/api-types"; -import { PackageScaleInfo } from "./composite-scale"; +import type { UnitLong } from "@macrostrat/api-types"; import { scaleLinear } from "d3-scale"; -import { getUnitHeightRange } from "@macrostrat/column-views"; +import { getUnitHeightRange } from "./utils"; import { ColumnAxisType } from "@macrostrat/column-components"; import { mergeAgeRanges, MergeMode } from "@macrostrat/stratigraphy-utils"; - -export enum HybridScaleType { - // An age-domain scale that puts equal vertical space between surfaces - EquidistantSurfaces = "equidistant-surfaces", - // A height-domain scale that is based on the average height of units between surfaces - ApproximateHeight = "approximate-height", -} - -interface HybridScaleOptions { - pixelOffset?: number; - pixelScale?: number; -} - -type ApproxHeightScaleOptions = { - minHeight?: number; - defaultHeight?: number; - heightMethod?: HeightMethod; -}; - -export type HybridScaleDefinition = - | ({ - type: HybridScaleType.ApproximateHeight; - } & ApproxHeightScaleOptions) - | { - type: HybridScaleType.EquidistantSurfaces; - }; +import type { + ApproxHeightScaleOptions, + ExtUnit, + HybridScaleDefinition, + HybridScaleOptions, + PackageScaleInfo, +} from "./types"; +import { HybridScaleType, HeightMethod } from "./types"; interface BaseSurface { index: number; @@ -161,12 +141,6 @@ export function buildHybridScale( return buildApproximateHeightScale(s1, units, { ...options, ...rest }); } -export enum HeightMethod { - Minimum = "minimum", - Average = "average", - Maximum = "maximum", -} - function getApproximateHeight( unit: ExtUnit, method: HeightMethod = HeightMethod.Maximum, diff --git a/packages/column-views/src/prepare-units/helpers.ts b/packages/column-views/src/prepare-units/helpers.ts index 61143e17..f022ea3e 100644 --- a/packages/column-views/src/prepare-units/helpers.ts +++ b/packages/column-views/src/prepare-units/helpers.ts @@ -12,36 +12,10 @@ import { AgeRangeRelationship, compareAgeRanges, } from "@macrostrat/stratigraphy-utils"; +import type { ExtUnit, SectionInfo, StratigraphicPackage } from "./types"; const dt = 0.001; -export interface StratigraphicPackage { - /** A collection of stratigraphic information organized in time, corresponding - * to single or multiple columns. */ - t_age: number; - b_age: number; -} - -export interface SectionInfo - extends StratigraphicPackage { - /** A time-bounded part of a single stratigraphic column. */ - section_id: number | number[]; - units: T[]; - b_pos?: number; - t_pos?: number; -} - -export interface ExtUnit extends UnitLong { - bottomOverlap: boolean; - overlappingUnits: number[]; - column?: number; - /* Positions (ages or heights) where the unit is clipped to its containing section. - * This is relevant if we are filtering by age/height/depth range. - */ - t_clip_pos?: number; - b_clip_pos?: number; -} - export function preprocessUnits( section: SectionInfo, axisType: ColumnAxisType = ColumnAxisType.AGE, diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index 18081c80..dd9e9513 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -25,6 +25,7 @@ import { } from "./utils"; export * from "./utils"; +export * from "./types"; export { preprocessUnits }; export function usePreparedColumnUnits( diff --git a/packages/column-views/src/prepare-units/types.ts b/packages/column-views/src/prepare-units/types.ts new file mode 100644 index 00000000..4ded00b2 --- /dev/null +++ b/packages/column-views/src/prepare-units/types.ts @@ -0,0 +1,135 @@ +import type { UnitLong } from "@macrostrat/api-types"; +import type { ColumnAxisType } from "@macrostrat/column-components"; +import type { ScaleContinuousNumeric } from "d3-scale"; + +export interface ColumnHeightScaleOptions { + /** A fixed pixel scale to use for the section (pixels per Myr) */ + pixelScale?: number; + /** The target height of a constituent unit in pixels, for dynamic + * scale generation */ + targetUnitHeight?: number; + /** Min height of a section in pixels. Will override minPixelScale in some cases. */ + minSectionHeight?: number; + /** The minimum pixel scale to use for the section (pixels per Myr). This is mostly + * needed because small sections (<1-2 units) don't necessarily have space to comfortably + * render two axis labels */ + minPixelScale?: number; + // Axis scale type + axisType?: ColumnAxisType; + // Unconformity height in pixels + unconformityHeight?: number; + // Whether to collapse unconformities that are less than a height threshold + collapseSmallUnconformities?: boolean | number; + // A continuous scale to use instead of generating one + // TODO: discontinuous scales are not yet supported + scale?: ScaleContinuousNumeric; + // Hybrid scale type. + // This overrides parameters such as the axis type + hybridScale?: HybridScaleDefinition; +} + +export enum HybridScaleType { + // An age-domain scale that puts equal vertical space between surfaces + EquidistantSurfaces = "equidistant-surfaces", + // A height-domain scale that is based on the average height of units between surfaces + ApproximateHeight = "approximate-height", +} + +export enum HeightMethod { + Minimum = "minimum", + Average = "average", + Maximum = "maximum", +} + +export interface HybridScaleOptions { + pixelOffset?: number; + pixelScale?: number; +} + +export type ApproxHeightScaleOptions = { + minHeight?: number; + defaultHeight?: number; + heightMethod?: HeightMethod; +}; + +export type HybridScaleDefinition = + | ({ + type: HybridScaleType.ApproximateHeight; + } & ApproxHeightScaleOptions) + | { + type: HybridScaleType.EquidistantSurfaces; + }; + +export interface SectionScaleOptions extends ColumnHeightScaleOptions { + axisType: ColumnAxisType; + domain: [number, number]; +} + +/** Output of a section scale. For now, this assumes that the + * mapping is linear, but it could be extended to support arbitrary + * scale functions. + */ +export interface PackageScaleInfo { + domain: [number, number]; + pixelHeight: number; + // TODO: add a function + scale: ScaleContinuousNumeric; + pixelScale?: number; // if it's a linear scale, this could be defined + // Subsidiary scale for height mapping (for hybrid scales) + heightScale?: ScaleContinuousNumeric; +} + +export type PackageScaleLayoutData = PackageScaleInfo & { + // A unique key for the section to use in React + key: string; + offset: number; + // How much to + paddingTop: number; +}; +export interface StratigraphicPackage { + /** A collection of stratigraphic information organized in time, corresponding + * to single or multiple columns. */ + t_age: number; + b_age: number; +} + +export interface SectionInfo + extends StratigraphicPackage { + /** A time-bounded part of a single stratigraphic column. */ + section_id: number | number[]; + units: T[]; + b_pos?: number; + t_pos?: number; +} + +export interface ExtUnit extends UnitLong { + bottomOverlap: boolean; + overlappingUnits: number[]; + column?: number; + /* Positions (ages or heights) where the unit is clipped to its containing section. + * This is relevant if we are filtering by age/height/depth range. + */ + t_clip_pos?: number; + b_clip_pos?: number; +} + +export type PackageLayoutData = SectionInfo & { + scaleInfo: PackageScaleLayoutData; + // A unique key for the section to use in React + key: string; +}; + +export interface CompositeScaleData { + totalHeight: number; + sections: PackageScaleLayoutData[]; +} + +export interface ColumnScaleOptions extends ColumnHeightScaleOptions { + axisType: ColumnAxisType; + unconformityHeight: number; +} + +export interface CompositeColumnData + extends Omit { + sections: PackageLayoutData[]; +} diff --git a/packages/column-views/src/prepare-units/utils.ts b/packages/column-views/src/prepare-units/utils.ts index a9d4c99b..81f2ad89 100644 --- a/packages/column-views/src/prepare-units/utils.ts +++ b/packages/column-views/src/prepare-units/utils.ts @@ -4,14 +4,14 @@ import { compareAgeRanges, } from "@macrostrat/stratigraphy-utils"; import { ColumnAxisType } from "@macrostrat/column-components"; -import { type ExtUnit, StratigraphicPackage } from "./helpers"; -import { +import { ScaleContinuousNumeric } from "d3-scale"; +import type { ColumnScaleOptions, CompositeColumnData, + ExtUnit, PackageLayoutData, -} from "./composite-scale"; -import { ScaleContinuousNumeric } from "d3-scale"; -import { HybridScaleType } from "./dynamic-scales"; + StratigraphicPackage, +} from "./types"; const dt = 0.001; diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index ccf1253e..e97325a7 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -12,8 +12,6 @@ import { import { ColumnAxisType, SVG } from "@macrostrat/column-components"; import hyper from "@macrostrat/hyper"; import styles from "./column.module.sass"; -import type { ExtUnit } from "./prepare-units/helpers"; -import { PackageScaleLayoutData } from "./prepare-units/composite-scale"; import { useMacrostratColumnData, useMacrostratUnits, @@ -22,6 +20,7 @@ import { } from "./data-provider"; import { Duration } from "./unit-details"; import { Value } from "@macrostrat/data-components"; +import type { ExtUnit, PackageScaleLayoutData } from "./prepare-units/types"; const h = hyper.styled(styles); diff --git a/packages/column-views/src/unit-details/panel.ts b/packages/column-views/src/unit-details/panel.ts index c0fc3831..ef86bd05 100644 --- a/packages/column-views/src/unit-details/panel.ts +++ b/packages/column-views/src/unit-details/panel.ts @@ -15,13 +15,11 @@ import { Value, } from "@macrostrat/data-components"; import { - useColumnUnitsIfAvailable, useColumnUnitsMap, - useMacrostratColumnData, useMacrostratData, useMacrostratDefs, } from "../data-provider"; -import { +import type { Environment, UnitLong, UnitLongFull, diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index 7924e265..2d7cd384 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -14,7 +14,7 @@ import { StandaloneColumn, StandaloneColumnProps } from "./column-ui"; import { MinimalUnit } from "../src/units/boxes"; import { scaleLinear, scaleLog, scalePow } from "d3-scale"; import { ColumnAxisType } from "@macrostrat/column-components"; -import { HybridScaleType } from "../src/prepare-units/dynamic-scales"; +import { HybridScaleType } from "../src/prepare-units"; const h = hyper.styled(styles); diff --git a/packages/map-styles/src/style-image-manager/index.ts b/packages/map-styles/src/style-image-manager/index.ts index 1f21d77d..d3138f77 100644 --- a/packages/map-styles/src/style-image-manager/index.ts +++ b/packages/map-styles/src/style-image-manager/index.ts @@ -172,7 +172,7 @@ async function resolveFGDCImage( const num = parseInt(name); let patternName = name; - if (num == NaN) { + if (Number.isNaN(num)) { throw new Error(`Invalid FGDC pattern name: ${name}`); } if (num <= 599) { diff --git a/packages/timescale/src/index.ts b/packages/timescale/src/index.ts index 7b7d8027..ff4cf278 100644 --- a/packages/timescale/src/index.ts +++ b/packages/timescale/src/index.ts @@ -1,6 +1,6 @@ import { defaultIntervals } from "./intervals"; import { TimescaleProvider, useTimescale } from "./provider"; -import { Interval, TimescaleOrientation } from "./types"; +import { Interval, TimescaleOrientation, IncreaseDirection } from "./types"; import { TimescaleBoxes, Cursor, @@ -11,16 +11,20 @@ import { nestTimescale } from "./preprocess"; import { AgeAxis, AgeAxisProps } from "./age-axis"; import classNames from "classnames"; import { ScaleContinuousNumeric } from "d3-scale"; -import { useMemo } from "react"; +import { ReactNode, useMemo } from "react"; import h from "./hyper"; +export * from "./intervals-api"; +export type { Interval } from "./types"; +export { + IncreaseDirection, + TimescaleOrientation, + defaultIntervals as intervals, +}; + type ClickHandler = (event: Event, interval: any) => void; -export enum IncreaseDirection { - UP_RIGHT = "up-right", - DOWN_LEFT = "down-left", -} -interface TimescaleProps { +export interface TimescaleProps { intervals?: Interval[]; orientation?: TimescaleOrientation; increaseDirection?: IncreaseDirection; @@ -43,12 +47,12 @@ interface TimescaleProps { function TimescaleContainer(props: { onClick?: ClickHandler; className: string; - children?: React.ReactNode; + children?: ReactNode; }) { const { onClick: clickHandler, ...rest } = props; const { scale, orientation } = useTimescale(); - function onClick(evt) { + function onClick(evt: any) { const bbox = evt.currentTarget.getBoundingClientRect(); const pos = orientation == TimescaleOrientation.HORIZONTAL @@ -60,7 +64,7 @@ function TimescaleContainer(props: { return h("div.timescale.timescale-container", { onClick, ...rest }); } -function Timescale(props: TimescaleProps) { +export function Timescale(props: TimescaleProps) { /** * A geologic timescale component for react. * @@ -127,11 +131,3 @@ function Timescale(props: TimescaleProps) { ]), ); } - -export * from "./intervals-api"; -export { - Timescale, - TimescaleOrientation, - TimescaleProps, - defaultIntervals as intervals, -}; diff --git a/packages/timescale/src/types.ts b/packages/timescale/src/types.ts index 0cd69228..d8985fc9 100644 --- a/packages/timescale/src/types.ts +++ b/packages/timescale/src/types.ts @@ -1,4 +1,4 @@ -import { ScaleContinuousNumeric, ScaleLinear } from "d3-scale"; +import type { ScaleContinuousNumeric } from "d3-scale"; export interface Interval { pid: number | null; @@ -20,11 +20,16 @@ interface NestedInterval extends Interval { type IntervalMap = Map; -enum TimescaleOrientation { +export enum TimescaleOrientation { VERTICAL = "vertical", HORIZONTAL = "horizontal", } +export enum IncreaseDirection { + UP_RIGHT = "up-right", + DOWN_LEFT = "down-left", +} + interface TimescaleProviderProps { timescale: NestedInterval; selectedInterval: Interval | null; @@ -39,4 +44,4 @@ interface TimescaleCTX extends TimescaleProviderProps { scale: ScaleContinuousNumeric; } -export { TimescaleCTX, NestedInterval, IntervalMap, TimescaleOrientation }; +export { TimescaleCTX, NestedInterval, IntervalMap }; From fcc29ec831f00e2f66622393480fe7e2f663f257 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 05:32:21 -0600 Subject: [PATCH 41/46] Fixed xDD panel --- packages/data-sheet/src/provider.ts | 3 +- .../location-details/xdd-panel/Article.tsx | 69 ------------------- .../src/location-details/xdd-panel/article.ts | 67 ++++++++++++++++++ .../src/location-details/xdd-panel/index.ts | 19 +---- .../xdd-panel/{Journal.tsx => journal.ts} | 20 ++++-- packages/timescale/src/provider.ts | 8 +-- packages/timescale/src/types.ts | 5 +- 7 files changed, 90 insertions(+), 101 deletions(-) delete mode 100644 packages/map-interface/src/location-details/xdd-panel/Article.tsx create mode 100644 packages/map-interface/src/location-details/xdd-panel/article.ts rename packages/map-interface/src/location-details/xdd-panel/{Journal.tsx => journal.ts} (76%) diff --git a/packages/data-sheet/src/provider.ts b/packages/data-sheet/src/provider.ts index 55abd896..579c68e6 100644 --- a/packages/data-sheet/src/provider.ts +++ b/packages/data-sheet/src/provider.ts @@ -7,9 +7,8 @@ import type { Region, Table2, } from "@blueprintjs/table"; -import { generateColumnSpec } from "./utils"; +import { generateColumnSpec, range } from "./utils"; import update, { Spec } from "immutability-helper"; -import { range } from "./utils"; import React from "react"; export interface ColumnSpec { diff --git a/packages/map-interface/src/location-details/xdd-panel/Article.tsx b/packages/map-interface/src/location-details/xdd-panel/Article.tsx deleted file mode 100644 index ee78b260..00000000 --- a/packages/map-interface/src/location-details/xdd-panel/Article.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import React, { useState } from "react"; -import { Collapse, Button } from "@blueprintjs/core"; -import { AuthorList } from "@macrostrat/ui-components"; -import h from "@macrostrat/hyper"; - -function Article(props) { - const [expanded, setExpanded] = useState(false); - const { data } = props; - - const toggleExpand = () => { - setExpanded(!expanded); - }; - - // Attempt to pull out only the year and not the whole date - let year; - try { - year = data.coverDate ? data.coverDate.match(/\d{4}/)[0] : ""; - } catch (e) { - year = ""; - } - - const authors = data?.authors?.split("; ") ?? []; - - const authorList = - authors.length > 0 ? h(AuthorList, { names: authors }) : "Unknown"; - - const iconName = expanded ? "chevron-up" : "chevron-down"; - - return ( -
-
-

- {authorList}, {year.length ? " " + year + ". " : ""} -

- - {data.title}. - - - - - -
- - -
- {data.highlight.map((snippet, si) => { - let text = snippet; - - return ( -

- ); - })} -
-
-
-
- ); -} - -export default Article; diff --git a/packages/map-interface/src/location-details/xdd-panel/article.ts b/packages/map-interface/src/location-details/xdd-panel/article.ts new file mode 100644 index 00000000..910e7120 --- /dev/null +++ b/packages/map-interface/src/location-details/xdd-panel/article.ts @@ -0,0 +1,67 @@ +import React, { useState } from "react"; +import { Collapse, Button } from "@blueprintjs/core"; +import { AuthorList } from "@macrostrat/ui-components"; +import h from "@macrostrat/hyper"; + +export function Article(props) { + const [expanded, setExpanded] = useState(false); + const { data } = props; + + const toggleExpand = () => { + setExpanded(!expanded); + }; + + // Attempt to pull out only the year and not the whole date + let year; + try { + year = data.coverDate ? data.coverDate.match(/\d{4}/)[0] : ""; + } catch (e) { + year = ""; + } + + const authors = data?.authors?.split("; ") ?? []; + + const authorList = + authors.length > 0 ? h(AuthorList, { names: authors }) : "Unknown"; + + const iconName = expanded ? "chevron-up" : "chevron-down"; + + return h("div.article", [ + h("div.article-title", [ + h("p.article-author", [authorList, year.length ? ` ${year}. ` : ""]), + h( + "a.title-link", + { href: data.URL, target: "_blank" }, + h("strong", [data.title + "."]), + ), + h( + "span", + {}, + h(Button, { + onClick: toggleExpand, + minimal: true, + rightIcon: iconName, + className: "flat-btn", + }), + ), + ]), + h( + Collapse, + { isOpen: expanded }, + h( + "span", + { className: expanded ? "" : "hidden" }, + h( + "div.quotes", + {}, + data.highlight.map((snippet, si) => + h("p.gdd-snippet", { + key: si, + dangerouslySetInnerHTML: { __html: `...${snippet}...` }, + }), + ), + ), + ), + ), + ]); +} diff --git a/packages/map-interface/src/location-details/xdd-panel/index.ts b/packages/map-interface/src/location-details/xdd-panel/index.ts index 86b0439f..3afd259e 100644 --- a/packages/map-interface/src/location-details/xdd-panel/index.ts +++ b/packages/map-interface/src/location-details/xdd-panel/index.ts @@ -1,21 +1,8 @@ import { Spinner } from "@blueprintjs/core"; import h from "@macrostrat/hyper"; -import Journal from "./Journal"; +import { XDDSnippet, JournalLegacy } from "./journal"; import { ExpansionPanel } from "@macrostrat/map-interface"; -export interface XDDSnippet { - pubname: string; - publisher: string; - _gddid: string; - title: string; - doi: string; - coverDate: string; - URL: string; - authors: string; - hits: number; - highlight: string[]; -} - export function XddExpansion({ xddInfo, expanded = false, @@ -39,8 +26,6 @@ export function xDDPanelCore({ }) { const groupedData = groupSnippetsByJournal(xddInfo); - console.log("expanded", expanded); - return h( ExpansionPanel, { @@ -54,7 +39,7 @@ export function xDDPanelCore({ h.if(isFetching)(Spinner), h.if(!isFetching && xddInfo.length > 0)([ Array.from(groupedData.entries())?.map(([journal, snippets]) => { - return h(Journal, { + return h(JournalLegacy, { nestedExpanded, name: journal, articles: snippets, diff --git a/packages/map-interface/src/location-details/xdd-panel/Journal.tsx b/packages/map-interface/src/location-details/xdd-panel/journal.ts similarity index 76% rename from packages/map-interface/src/location-details/xdd-panel/Journal.tsx rename to packages/map-interface/src/location-details/xdd-panel/journal.ts index 2ebe203b..bcf5ab02 100644 --- a/packages/map-interface/src/location-details/xdd-panel/Journal.tsx +++ b/packages/map-interface/src/location-details/xdd-panel/journal.ts @@ -1,8 +1,20 @@ -import Article from "./Article"; +import { Article } from "./article"; import { Divider } from "@blueprintjs/core"; import h from "@macrostrat/hyper"; import { SubExpansionPanel } from "@macrostrat/map-interface"; -import { XDDSnippet } from "./index"; + +export interface XDDSnippet { + pubname: string; + publisher: string; + _gddid: string; + title: string; + doi: string; + coverDate: string; + URL: string; + authors: string; + hits: number; + highlight: string[]; +} function Journal(props) { return h("div.journal", [ @@ -27,7 +39,7 @@ type JournalProps = { }; // Still up for review -function Journal_(props: JournalProps) { +export function JournalLegacy(props: JournalProps) { const { articles, name, publisher, nestedExpanded } = props; const helpText = articles[0].pubname; @@ -46,5 +58,3 @@ function Journal_(props: JournalProps) { ], ); } - -export default Journal_; diff --git a/packages/timescale/src/provider.ts b/packages/timescale/src/provider.ts index 6d21e7a2..5d014703 100644 --- a/packages/timescale/src/provider.ts +++ b/packages/timescale/src/provider.ts @@ -1,13 +1,13 @@ import h from "@macrostrat/hyper"; import { scaleLinear } from "@visx/scale"; -import { createContext, useContext } from "react"; -import { TimescaleCTX, TimescaleOrientation } from "./types"; -import { IncreaseDirection } from "./index"; +import { createContext, useContext, type ReactNode } from "react"; +import type { TimescaleCTX } from "./types"; +import { TimescaleOrientation, IncreaseDirection } from "./types"; const TimescaleContext = createContext(null); interface TimescaleProviderProps extends TimescaleCTX { - children: React.ReactNode; + children: ReactNode; increaseDirection?: IncreaseDirection; } diff --git a/packages/timescale/src/types.ts b/packages/timescale/src/types.ts index d8985fc9..320f9a15 100644 --- a/packages/timescale/src/types.ts +++ b/packages/timescale/src/types.ts @@ -30,7 +30,7 @@ export enum IncreaseDirection { DOWN_LEFT = "down-left", } -interface TimescaleProviderProps { +interface TimescaleCTX { timescale: NestedInterval; selectedInterval: Interval | null; parentMap: IntervalMap; @@ -38,9 +38,6 @@ interface TimescaleProviderProps { length: number; orientation: TimescaleOrientation; levels: [number, number] | null; -} - -interface TimescaleCTX extends TimescaleProviderProps { scale: ScaleContinuousNumeric; } From f6e554dbd1c87769ec5276ead13a18d67144f527 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 06:20:54 -0600 Subject: [PATCH 42/46] Updated changelogs --- packages/api-types/CHANGELOG.md | 4 ++++ packages/api-types/package.json | 2 +- packages/color-utils/CHANGELOG.md | 4 ++++ packages/color-utils/package.json | 2 +- packages/column-components/package.json | 2 +- packages/column-views/package.json | 2 +- packages/column-views/src/age-axis.ts | 2 +- packages/column-views/src/correlation-chart/main.ts | 2 +- packages/column-views/src/prepare-units/index.ts | 2 +- scripts/publish-helpers/status.ts | 2 +- 10 files changed, 16 insertions(+), 8 deletions(-) diff --git a/packages/api-types/CHANGELOG.md b/packages/api-types/CHANGELOG.md index ca73bc3d..e66b5881 100644 --- a/packages/api-types/CHANGELOG.md +++ b/packages/api-types/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.3] - 2025-11-28 + +- Update `MacrostratInterval` and `Interval` types + ## [1.1.2] - 2025-08-22 - Added types for `StratName` and `StratNameConcept` diff --git a/packages/api-types/package.json b/packages/api-types/package.json index 9f411ca1..25754d79 100644 --- a/packages/api-types/package.json +++ b/packages/api-types/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/api-types", - "version": "1.1.2", + "version": "1.1.3", "description": "Type definitions for Macrostrat's API", "main": "./src/index.d.ts", "types": "./src/index.d.ts", diff --git a/packages/color-utils/CHANGELOG.md b/packages/color-utils/CHANGELOG.md index 02ee67ab..eb4ed061 100644 --- a/packages/color-utils/CHANGELOG.md +++ b/packages/color-utils/CHANGELOG.md @@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.2] - 2025-11-28 + +Updated Parcel bundler + ## [1.1.1] - 2025-06-25 Add `delta` parameter to `getLuminanceAdjustedColorScheme` to control the diff --git a/packages/color-utils/package.json b/packages/color-utils/package.json index 0cc22056..7ef5d80f 100644 --- a/packages/color-utils/package.json +++ b/packages/color-utils/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/color-utils", - "version": "1.1.1", + "version": "1.1.2", "description": "Color utility functions", "type": "module", "main": "dist/index.js", diff --git a/packages/column-components/package.json b/packages/column-components/package.json index 0d35b4b8..64661e87 100644 --- a/packages/column-components/package.json +++ b/packages/column-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/column-components", - "version": "1.3.0", + "version": "1.3.1", "description": "React rendering primitives for stratigraphic columns", "keywords": [ "geology", diff --git a/packages/column-views/package.json b/packages/column-views/package.json index af177eff..5412996e 100644 --- a/packages/column-views/package.json +++ b/packages/column-views/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/column-views", - "version": "2.1.4", + "version": "2.2.0", "description": "Data views for Macrostrat stratigraphic columns", "type": "module", "source": "src/index.ts", diff --git a/packages/column-views/src/age-axis.ts b/packages/column-views/src/age-axis.ts index 2dfe0b31..52213a3c 100644 --- a/packages/column-views/src/age-axis.ts +++ b/packages/column-views/src/age-axis.ts @@ -10,8 +10,8 @@ import { useContext } from "react"; import styles from "./age-axis.module.sass"; import { useCompositeScale, useMacrostratColumnData } from "./data-provider"; import { Parenthetical } from "@macrostrat/data-components"; -import { PackageScaleLayoutData } from "./prepare-units/composite-scale"; import { AgeLabel } from "./unit-details"; +import { PackageScaleLayoutData } from "./prepare-units/types"; const h = hyper.styled(styles); diff --git a/packages/column-views/src/correlation-chart/main.ts b/packages/column-views/src/correlation-chart/main.ts index 2b5fa196..9ebe7120 100644 --- a/packages/column-views/src/correlation-chart/main.ts +++ b/packages/column-views/src/correlation-chart/main.ts @@ -29,11 +29,11 @@ import { } from "@macrostrat/column-components"; import { ColoredUnitComponent } from "../units"; import { UnitBoxes } from "../units/boxes"; -import { ExtUnit } from "../prepare-units/helpers"; import { ColumnContainer } from "../column"; import { ColumnData } from "../data-provider"; import { BaseUnit } from "@macrostrat/api-types"; import { ScaleContinuousNumeric } from "d3-scale"; +import { ExtUnit } from "@macrostrat/column-views"; const h = hyper.styled(styles); diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index dd9e9513..ceb5cc25 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -15,7 +15,6 @@ import { computeSectionHeights, finalizeSectionHeights, } from "./composite-scale"; -import type { SectionInfo } from "./helpers"; import { agesOverlap, MergeSectionsMode, @@ -23,6 +22,7 @@ import { PreparedColumnData, unitsOverlap, } from "./utils"; +import { SectionInfo } from "@macrostrat/column-views"; export * from "./utils"; export * from "./types"; diff --git a/scripts/publish-helpers/status.ts b/scripts/publish-helpers/status.ts index 01d62ecc..abd5772e 100644 --- a/scripts/publish-helpers/status.ts +++ b/scripts/publish-helpers/status.ts @@ -246,7 +246,7 @@ function printChangeInfoForPublishedPackage(pkg, showChanges = false) { } console.log("Run the following command to see detailed changes:"); - console.log(chalk.dim(">"), chalk.dim(cmd)); + console.log(chalk.dim(cmd)); // Check if is synced with the remote // TODO: this only works if the current branch is pushed to the remote From 65f2a8860afb2c241e027dcb3017909f3325a4f0 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 18:39:19 -0600 Subject: [PATCH 43/46] Updated changelogs and package versions --- packages/column-components/CHANGELOG.md | 5 +++++ packages/column-views/CHANGELOG.md | 7 +++++++ packages/data-components/CHANGELOG.md | 6 ++++++ packages/data-components/package.json | 2 +- packages/data-sheet/CHANGELOG.md | 4 ++++ packages/data-sheet/package.json | 2 +- packages/feedback-components/CHANGELOG.md | 4 ++++ packages/feedback-components/package.json | 2 +- packages/form-components/CHANGELOG.md | 4 ++++ packages/form-components/package.json | 2 +- packages/map-interface/CHANGELOG.md | 7 +++++++ packages/map-interface/package.json | 2 +- packages/map-styles/CHANGELOG.md | 4 ++++ packages/map-styles/package.json | 2 +- packages/mapbox-react/CHANGELOG.md | 4 ++++ packages/mapbox-react/package.json | 2 +- packages/mapbox-utils/CHANGELOG.md | 4 ++++ packages/mapbox-utils/package.json | 2 +- packages/static-map-utils/CHANGELOG.md | 6 ++++++ packages/static-map-utils/package.json | 2 +- packages/stratigraphy-utils/CHANGELOG.md | 4 ++++ packages/stratigraphy-utils/package.json | 2 +- packages/timescale/CHANGELOG.md | 9 +++++++++ packages/timescale/package.json | 2 +- packages/ui-components/CHANGELOG.md | 4 ++++ packages/ui-components/package.json | 2 +- toolchain/hyperstyle-loader/CHANGELOG.md | 6 +++++- toolchain/hyperstyle-loader/package.json | 2 +- toolchain/vite-plugin-hyperstyles/CHANGELOG.md | 4 ++++ toolchain/vite-plugin-hyperstyles/package.json | 2 +- 30 files changed, 95 insertions(+), 15 deletions(-) diff --git a/packages/column-components/CHANGELOG.md b/packages/column-components/CHANGELOG.md index 506fdb94..c2a2e4bc 100644 --- a/packages/column-components/CHANGELOG.md +++ b/packages/column-components/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.3.1] - 2025-11-28 + +- Update axis components +- Improve handling on nonlinear and discontinuous axes + ## [1.3.0] - 2025-10-29 - Switch to `@visx/axis` for axis rendering diff --git a/packages/column-views/CHANGELOG.md b/packages/column-views/CHANGELOG.md index 844a4305..e1dd72cc 100644 --- a/packages/column-views/CHANGELOG.md +++ b/packages/column-views/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.0] - 2025-11-28 + +- Update SGP and PBDB facets +- Improve `UnitDetailsPanel` styling and information content +- Improve use of discontinous scales +- Create `hybridScale` options block to allow more dynamic scale generation + ## [2.1.4] - 2025-10-29 - Improve stories diff --git a/packages/data-components/CHANGELOG.md b/packages/data-components/CHANGELOG.md index c65a49ae..c67d4567 100644 --- a/packages/data-components/CHANGELOG.md +++ b/packages/data-components/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## [0.2.2] - 2025-11-28 + +- Move location information (e.g., `LngLatCoords`, `Elevation`) React components + into this module +- Add a `--unit-color` CSS variable + ## [0.2.1] - 2025-08-22 - Improvements to Rockd checkin component diff --git a/packages/data-components/package.json b/packages/data-components/package.json index 6e4e61db..22c6f814 100644 --- a/packages/data-components/package.json +++ b/packages/data-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/data-components", - "version": "0.2.1", + "version": "0.2.2", "description": "A library of React components tailored for Macrostrat data and endpoints", "type": "module", "source": "src/index.ts", diff --git a/packages/data-sheet/CHANGELOG.md b/packages/data-sheet/CHANGELOG.md index 4ee36576..c5843cd0 100644 --- a/packages/data-sheet/CHANGELOG.md +++ b/packages/data-sheet/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.2.1] - 2025-11-28 + +Internal fixes + ## [2.2.0] - 2025-10-29 - PostgREST sheet has full table search ability diff --git a/packages/data-sheet/package.json b/packages/data-sheet/package.json index bf2eac7a..8c856b4c 100644 --- a/packages/data-sheet/package.json +++ b/packages/data-sheet/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/data-sheet", - "version": "2.2.0", + "version": "2.2.1", "description": "Scalable data sheet with optional editing capabilities", "type": "module", "source": "src/index.ts", diff --git a/packages/feedback-components/CHANGELOG.md b/packages/feedback-components/CHANGELOG.md index ef20ee6b..925c77d7 100644 --- a/packages/feedback-components/CHANGELOG.md +++ b/packages/feedback-components/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.10] - 2025-11-28 + +- Upgrade `parcel` bundler + ## [1.1.9] - 2025-08-13 - Allow no model (user feedback) diff --git a/packages/feedback-components/package.json b/packages/feedback-components/package.json index 564a1e17..df67fe6b 100644 --- a/packages/feedback-components/package.json +++ b/packages/feedback-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/feedback-components", - "version": "1.1.9", + "version": "1.1.10", "description": "", "source": "src/index.ts", "type": "module", diff --git a/packages/form-components/CHANGELOG.md b/packages/form-components/CHANGELOG.md index 49db1bb9..d8ac8776 100644 --- a/packages/form-components/CHANGELOG.md +++ b/packages/form-components/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.2.5] - 2025-11-28 + +- Upgrade `parcel` bundler + ## [0.2.4] - 2025-08-08 Add LexSelection component diff --git a/packages/form-components/package.json b/packages/form-components/package.json index 1ecf96f0..49c37517 100644 --- a/packages/form-components/package.json +++ b/packages/form-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/form-components", - "version": "0.2.4", + "version": "0.2.5", "description": "Form components for user input into Macrostrat apps", "type": "module", "source": "src/index.ts", diff --git a/packages/map-interface/CHANGELOG.md b/packages/map-interface/CHANGELOG.md index 766c286c..8a7aa77f 100644 --- a/packages/map-interface/CHANGELOG.md +++ b/packages/map-interface/CHANGELOG.md @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.0] - 2025-11-28 + +- Update XDD panel components to remove Typescript +- Remove circular dependencies +- Move `LngLatCoords` and `Elevation` components to + `@macrostrat/data-components` + ## [1.5.7] - 2025-08-19 - Remove a duplicate export diff --git a/packages/map-interface/package.json b/packages/map-interface/package.json index 11553cf2..d5c19eff 100644 --- a/packages/map-interface/package.json +++ b/packages/map-interface/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/map-interface", - "version": "1.5.7", + "version": "1.6.0", "description": "Map interface for Macrostrat", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/map-styles/CHANGELOG.md b/packages/map-styles/CHANGELOG.md index ef5ebfc5..1d9e49cb 100644 --- a/packages/map-styles/CHANGELOG.md +++ b/packages/map-styles/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.4] - 2025-11-28 + +- Fix a NaN comparison bug in resolveFGDCImage + ## [1.2.3] - 2025-11-02 - Added `setupStyleImageManager` function and associated utilities to manage diff --git a/packages/map-styles/package.json b/packages/map-styles/package.json index 5bfe217f..7d283467 100644 --- a/packages/map-styles/package.json +++ b/packages/map-styles/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/map-styles", - "version": "1.2.3", + "version": "1.2.4", "description": "Utilities for working with Mapbox map styles", "main": "dist/index.js", "types": "dist/index.d.ts", diff --git a/packages/mapbox-react/CHANGELOG.md b/packages/mapbox-react/CHANGELOG.md index 2bb77cc7..603b5167 100644 --- a/packages/mapbox-react/CHANGELOG.md +++ b/packages/mapbox-react/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.6.4] - 2025-11-28 + +- Upgrade `parcel` bundler + ## [2.6.3] - 2025-08-19 - Improve typings for `useMapRef` hook diff --git a/packages/mapbox-react/package.json b/packages/mapbox-react/package.json index 1d4cd3d7..5eb6e9f9 100644 --- a/packages/mapbox-react/package.json +++ b/packages/mapbox-react/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/mapbox-react", - "version": "2.6.3", + "version": "2.6.4", "description": "Components to support using Mapbox maps in React", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/mapbox-utils/CHANGELOG.md b/packages/mapbox-utils/CHANGELOG.md index 58ddaa42..a2225d2b 100644 --- a/packages/mapbox-utils/CHANGELOG.md +++ b/packages/mapbox-utils/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.6.1] - 2025-11-28 + +- Update `parcel` bundler + ## [1.6.0] - 2025-07-02 - Add functions to manage focus state and map positioning, inherited from diff --git a/packages/mapbox-utils/package.json b/packages/mapbox-utils/package.json index 913f58ec..1e71411f 100644 --- a/packages/mapbox-utils/package.json +++ b/packages/mapbox-utils/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/mapbox-utils", - "version": "1.6.0", + "version": "1.6.1", "description": "Utilities for working with Mapbox maps", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/static-map-utils/CHANGELOG.md b/packages/static-map-utils/CHANGELOG.md index 883b6d44..59a9a604 100644 --- a/packages/static-map-utils/CHANGELOG.md +++ b/packages/static-map-utils/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 2025-11-28 + +- Remove map state watchers that duplicate `@macrostrat/mapbox-utils` + functionality +- Reorganize utility functions + ## [1.0.1] - 2025-11-02 - Move `setupStyleImageManager` function and associated utilities to diff --git a/packages/static-map-utils/package.json b/packages/static-map-utils/package.json index 619960c5..c329693f 100644 --- a/packages/static-map-utils/package.json +++ b/packages/static-map-utils/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/static-map-utils", - "version": "1.0.1", + "version": "1.0.2", "description": "Utilities for working with Mapbox maps", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/packages/stratigraphy-utils/CHANGELOG.md b/packages/stratigraphy-utils/CHANGELOG.md index a189983f..73ef9171 100644 --- a/packages/stratigraphy-utils/CHANGELOG.md +++ b/packages/stratigraphy-utils/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.0.2] - 2025-11-28 + +Improve the calculation of stratigraphic age ranges + ## [1.0.1] - 2025-06-25 Small bugfixes diff --git a/packages/stratigraphy-utils/package.json b/packages/stratigraphy-utils/package.json index e5513032..178d84da 100644 --- a/packages/stratigraphy-utils/package.json +++ b/packages/stratigraphy-utils/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/stratigraphy-utils", - "version": "1.0.1", + "version": "1.0.2", "description": "Utility functions for dealing with stratigraphy", "source": "src/index.ts", "type": "module", diff --git a/packages/timescale/CHANGELOG.md b/packages/timescale/CHANGELOG.md index 4ccf616d..ab4bcf68 100644 --- a/packages/timescale/CHANGELOG.md +++ b/packages/timescale/CHANGELOG.md @@ -6,6 +6,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.3.0] - 2025-11-28 + +- Allow label positioning to be more discretely controlled (e.g., turn off label + rotation) +- Add a `fetchMacrostratIntervals` and `buildIntervalsTree` function to get + timescale data from the Macrostrat API (only international intervals supported + for now) +- Remove circular imports + ## [2.2.2] - 2025-10-29 - Add the ability to specify your own scale for the timescale diff --git a/packages/timescale/package.json b/packages/timescale/package.json index d47af4c8..55fdf716 100644 --- a/packages/timescale/package.json +++ b/packages/timescale/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/timescale", - "version": "2.2.2", + "version": "2.3.0", "description": "A configurable geologic timescale written with React and Typescript", "type": "module", "source": "src/index.ts", diff --git a/packages/ui-components/CHANGELOG.md b/packages/ui-components/CHANGELOG.md index 599200b3..0e5b5236 100644 --- a/packages/ui-components/CHANGELOG.md +++ b/packages/ui-components/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [4.5.2] - 2025-11-28 + +Make `SizeAwareLabel` a little more configurable + ## [4.5.1] - 2025-11-02 Export `PatternFillSpec` type diff --git a/packages/ui-components/package.json b/packages/ui-components/package.json index fccbb3cd..898d9dcb 100644 --- a/packages/ui-components/package.json +++ b/packages/ui-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/ui-components", - "version": "4.5.1", + "version": "4.5.2", "description": "UI components for React and Blueprint.js", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/toolchain/hyperstyle-loader/CHANGELOG.md b/toolchain/hyperstyle-loader/CHANGELOG.md index 84488fdc..ae0765f8 100644 --- a/toolchain/hyperstyle-loader/CHANGELOG.md +++ b/toolchain/hyperstyle-loader/CHANGELOG.md @@ -1,8 +1,12 @@ # Changelog +## [1.0.2] - 2025-11-28 + +Update Parcel bundler + ## [1.0.1] - 2025-06-25 -Update Parcel bundling +Update Parcel bundler ## [1.0.0] - 2025-02-14 diff --git a/toolchain/hyperstyle-loader/package.json b/toolchain/hyperstyle-loader/package.json index 3b008189..b5834507 100644 --- a/toolchain/hyperstyle-loader/package.json +++ b/toolchain/hyperstyle-loader/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/hyperstyle-loader", - "version": "1.0.1", + "version": "1.0.2", "main": "index.js", "scripts": { "build": "echo 'Nothing to be done!'" diff --git a/toolchain/vite-plugin-hyperstyles/CHANGELOG.md b/toolchain/vite-plugin-hyperstyles/CHANGELOG.md index 9a17a35e..5a74d6d7 100644 --- a/toolchain/vite-plugin-hyperstyles/CHANGELOG.md +++ b/toolchain/vite-plugin-hyperstyles/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.0.3] - 2025-11-28 + +Update Parcel bundler. + ## [1.0.2] - 2025-06-25 Update required Vite version. diff --git a/toolchain/vite-plugin-hyperstyles/package.json b/toolchain/vite-plugin-hyperstyles/package.json index ce7c45c6..4f40a572 100644 --- a/toolchain/vite-plugin-hyperstyles/package.json +++ b/toolchain/vite-plugin-hyperstyles/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/vite-plugin-hyperstyles", - "version": "1.0.2", + "version": "1.0.3", "main": "dist/index.js", "types": "dist/index.d.ts", "source": "src/index.ts", From d43a241e35f6d26b79b546fae86d6e19cb6cd7b3 Mon Sep 17 00:00:00 2001 From: davenquinn Date: Sat, 29 Nov 2025 00:41:53 +0000 Subject: [PATCH 44/46] Apply formatting changes --- packages/column-components/src/context/layout.ts | 10 ++++++---- packages/column-views/src/column.ts | 4 +--- .../src/correlation-chart/prepare-data.ts | 6 ++++-- .../column-views/src/prepare-units/composite-scale.ts | 5 +++-- packages/column-views/src/prepare-units/types.ts | 11 +++++++---- packages/column-views/src/units/boxes.ts | 4 +--- packages/data-sheet/src/__archive/base.ts | 5 +++-- packages/data-sheet/src/postgrest-table/index.ts | 5 +++-- packages/ui-components/src/infinite-scroll.ts | 6 ++++-- 9 files changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/column-components/src/context/layout.ts b/packages/column-components/src/context/layout.ts index 24a0c158..6b60ca48 100644 --- a/packages/column-components/src/context/layout.ts +++ b/packages/column-components/src/context/layout.ts @@ -12,8 +12,9 @@ import { ReactNode } from "react"; //# This isn't really used yet... -export interface ColumnLayoutCtx - extends ColumnCtx { +export interface ColumnLayoutCtx< + T extends ColumnDivision, +> extends ColumnCtx { width: number; grainSizes?: string[]; grainsizeScale?: (d: string) => number; @@ -34,8 +35,9 @@ const ColumnLayoutContext = createContext>({ zoom: 1, }); -export interface ColumnLayoutProviderProps - extends Partial> { +export interface ColumnLayoutProviderProps< + T extends ColumnDivision, +> extends Partial> { grainSizes?: string[]; grainsizeScale?: (d: string) => number; width: number; diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 678813c5..f38c618e 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -69,9 +69,7 @@ interface BaseColumnProps extends SectionSharedProps { } export interface ColumnProps - extends Padding, - BaseColumnProps, - ColumnHeightScaleOptions { + extends Padding, BaseColumnProps, ColumnHeightScaleOptions { // Macrostrat units units: UnitLong[]; t_age?: number; diff --git a/packages/column-views/src/correlation-chart/prepare-data.ts b/packages/column-views/src/correlation-chart/prepare-data.ts index 07a47c8a..89ed80bf 100644 --- a/packages/column-views/src/correlation-chart/prepare-data.ts +++ b/packages/column-views/src/correlation-chart/prepare-data.ts @@ -21,8 +21,10 @@ interface ColumnData { units: UnitLong[]; } -export interface CorrelationChartSettings - extends Omit { +export interface CorrelationChartSettings extends Omit< + PrepareColumnOptions, + "axisType" +> { targetUnitHeight?: number; } diff --git a/packages/column-views/src/prepare-units/composite-scale.ts b/packages/column-views/src/prepare-units/composite-scale.ts index 7ab538ac..28f5cef1 100644 --- a/packages/column-views/src/prepare-units/composite-scale.ts +++ b/packages/column-views/src/prepare-units/composite-scale.ts @@ -84,8 +84,9 @@ export function finalizeSectionHeights( }; } -export interface SectionInfoWithScale - extends SectionInfo { +export interface SectionInfoWithScale< + T extends UnitLong = ExtUnit, +> extends SectionInfo { scaleInfo: PackageScaleInfo; } diff --git a/packages/column-views/src/prepare-units/types.ts b/packages/column-views/src/prepare-units/types.ts index 4ded00b2..65d6af09 100644 --- a/packages/column-views/src/prepare-units/types.ts +++ b/packages/column-views/src/prepare-units/types.ts @@ -93,8 +93,9 @@ export interface StratigraphicPackage { b_age: number; } -export interface SectionInfo - extends StratigraphicPackage { +export interface SectionInfo< + T extends UnitLong = ExtUnit, +> extends StratigraphicPackage { /** A time-bounded part of a single stratigraphic column. */ section_id: number | number[]; units: T[]; @@ -129,7 +130,9 @@ export interface ColumnScaleOptions extends ColumnHeightScaleOptions { unconformityHeight: number; } -export interface CompositeColumnData - extends Omit { +export interface CompositeColumnData extends Omit< + CompositeScaleData, + "sections" +> { sections: PackageLayoutData[]; } diff --git a/packages/column-views/src/units/boxes.ts b/packages/column-views/src/units/boxes.ts index 0a935539..e42ccd1f 100644 --- a/packages/column-views/src/units/boxes.ts +++ b/packages/column-views/src/units/boxes.ts @@ -45,9 +45,7 @@ interface UnitProps extends Clickable, Partial, UnitRectOptions { } export interface LabeledUnitProps - extends UnitRectOptions, - Clickable, - Partial { + extends UnitRectOptions, Clickable, Partial { division: IUnit; patternID?: string | number; label: string; diff --git a/packages/data-sheet/src/__archive/base.ts b/packages/data-sheet/src/__archive/base.ts index d039c946..cbc3139c 100644 --- a/packages/data-sheet/src/__archive/base.ts +++ b/packages/data-sheet/src/__archive/base.ts @@ -10,8 +10,9 @@ interface GridElement extends ReactDataSheet.Cell { value: number | null; } -interface SheetContainerProps - extends ReactDataSheet.DataSheetProps { +interface SheetContainerProps< + T = any, +> extends ReactDataSheet.DataSheetProps { height?: number; width?: number; children?: React.ReactNode; diff --git a/packages/data-sheet/src/postgrest-table/index.ts b/packages/data-sheet/src/postgrest-table/index.ts index 8ff4dc41..08636abd 100644 --- a/packages/data-sheet/src/postgrest-table/index.ts +++ b/packages/data-sheet/src/postgrest-table/index.ts @@ -32,8 +32,9 @@ export type GenericSchema = { Functions: Record; }; -interface PostgRESTTableViewProps - extends DataSheetProviderProps { +interface PostgRESTTableViewProps< + T extends object, +> extends DataSheetProviderProps { endpoint: string; table: string; columnOptions?: any; diff --git a/packages/ui-components/src/infinite-scroll.ts b/packages/ui-components/src/infinite-scroll.ts index b2096cf0..a8035983 100644 --- a/packages/ui-components/src/infinite-scroll.ts +++ b/packages/ui-components/src/infinite-scroll.ts @@ -25,8 +25,10 @@ type ScrollResponseItems = Pick< "count" | "hasMore" | "items" >; -export interface InfiniteScrollProps - extends Omit, "params"> { +export interface InfiniteScrollProps extends Omit< + APIResultProps, + "params" +> { getCount(r: T): number; getNextParams(r: T, params: QueryParams): QueryParams; getItems(r: T): any; From 5fb77a4ce1fa9bcc5dadad1e7da8f1b07ddbb146 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 19:18:18 -0600 Subject: [PATCH 45/46] Make sure @macrostrat/column-views builds --- packages/column-components/src/context/layout.ts | 2 +- packages/column-components/src/edit-overlay.ts | 2 +- .../column-components/src/editor/facies/color-picker.ts | 2 +- .../column-components/src/editor/facies/description.ts | 2 +- packages/column-components/src/editor/facies/picker.ts | 3 ++- packages/column-components/src/lithology/index.ts | 2 +- packages/column-components/src/notes/layout.ts | 2 +- packages/column-components/src/util/scroll-box.ts | 2 +- packages/column-views/src/correlation-chart/main.ts | 2 +- packages/column-views/src/facets/base-sample-column.ts | 8 +++----- packages/column-views/src/facets/detrital-zircon/index.ts | 4 ++-- packages/column-views/src/facets/fossils/index.ts | 4 ++-- packages/column-views/src/prepare-units/index.ts | 2 +- packages/column-views/src/units/selection.ts | 5 ----- packages/svg-map-components/src/canvas-layer.ts | 2 +- packages/svg-map-components/src/drag-interaction.ts | 2 +- packages/timescale/src/intervals-api.ts | 2 +- packages/timescale/src/provider.ts | 2 +- 18 files changed, 22 insertions(+), 28 deletions(-) diff --git a/packages/column-components/src/context/layout.ts b/packages/column-components/src/context/layout.ts index 24a0c158..dca7ef58 100644 --- a/packages/column-components/src/context/layout.ts +++ b/packages/column-components/src/context/layout.ts @@ -77,7 +77,7 @@ export interface CrossAxisLayoutProviderProps { class CrossAxisLayoutProvider extends Component { static contextType = ColumnContext; - context: ColumnCtx; + declare context: ColumnCtx; render() { let { domain, range, width, children } = this.props; if (range == null) { diff --git a/packages/column-components/src/edit-overlay.ts b/packages/column-components/src/edit-overlay.ts index 35993d0a..2aa8a56b 100644 --- a/packages/column-components/src/edit-overlay.ts +++ b/packages/column-components/src/edit-overlay.ts @@ -139,7 +139,7 @@ export class DivisionEditOverlay extends Component< }; timeout: any; - context: ColumnLayoutCtx; + declare context: ColumnLayoutCtx; constructor(props: DivisionEditOverlayProps) { super(props); this.onHoverInterval = this.onHoverInterval.bind(this); diff --git a/packages/column-components/src/editor/facies/color-picker.ts b/packages/column-components/src/editor/facies/color-picker.ts index 1eb14bbb..3c46b5d7 100644 --- a/packages/column-components/src/editor/facies/color-picker.ts +++ b/packages/column-components/src/editor/facies/color-picker.ts @@ -19,7 +19,7 @@ interface FaciesColorPickerProps { class FaciesColorPicker extends Component { static contextType = FaciesContext; - context: any; + declare context: any; render() { const { setFaciesColor } = this.context; diff --git a/packages/column-components/src/editor/facies/description.ts b/packages/column-components/src/editor/facies/description.ts index 022e55ac..6654b091 100644 --- a/packages/column-components/src/editor/facies/description.ts +++ b/packages/column-components/src/editor/facies/description.ts @@ -27,7 +27,7 @@ export class FaciesDescriptionSmall extends Component { static contextType = FaciesContext; - context: any; + declare context: any; + render() { const { facies } = this.context; const { interval, onChange } = this.props; diff --git a/packages/column-components/src/lithology/index.ts b/packages/column-components/src/lithology/index.ts index 2f79503b..a6ddb904 100644 --- a/packages/column-components/src/lithology/index.ts +++ b/packages/column-components/src/lithology/index.ts @@ -240,7 +240,7 @@ class LithologyBoxes extends UUIDComponent { resolveID: defaultResolveID, minimumHeight: 0, }; - context: ColumnLayoutCtx; + declare context: ColumnLayoutCtx; constructor(props) { super(props); this.constructLithologyDivisions = diff --git a/packages/column-components/src/notes/layout.ts b/packages/column-components/src/notes/layout.ts index c7ba031c..00337f84 100644 --- a/packages/column-components/src/notes/layout.ts +++ b/packages/column-components/src/notes/layout.ts @@ -104,7 +104,7 @@ class NoteLayoutProvider extends StatefulComponent< return 12; }, }; - context: ColumnCtx; + declare context: ColumnCtx; _previousContext: ColumnCtx; _rendererIndex: object; diff --git a/packages/column-components/src/util/scroll-box.ts b/packages/column-components/src/util/scroll-box.ts index 2c1668eb..86fed746 100644 --- a/packages/column-components/src/util/scroll-box.ts +++ b/packages/column-components/src/util/scroll-box.ts @@ -52,7 +52,7 @@ export class ColumnScroller extends Component { static contextType: Context> = ColumnContext; - context: ColumnCtx; + declare context: ColumnCtx; render() { const keys = [ diff --git a/packages/column-views/src/correlation-chart/main.ts b/packages/column-views/src/correlation-chart/main.ts index 9ebe7120..78ac315b 100644 --- a/packages/column-views/src/correlation-chart/main.ts +++ b/packages/column-views/src/correlation-chart/main.ts @@ -33,7 +33,7 @@ import { ColumnContainer } from "../column"; import { ColumnData } from "../data-provider"; import { BaseUnit } from "@macrostrat/api-types"; import { ScaleContinuousNumeric } from "d3-scale"; -import { ExtUnit } from "@macrostrat/column-views"; +import { ExtUnit } from "../prepare-units/types"; const h = hyper.styled(styles); diff --git a/packages/column-views/src/facets/base-sample-column.ts b/packages/column-views/src/facets/base-sample-column.ts index 2a875d10..b51edb91 100644 --- a/packages/column-views/src/facets/base-sample-column.ts +++ b/packages/column-views/src/facets/base-sample-column.ts @@ -1,11 +1,9 @@ -import { - ColumnNotes, - getUnitHeightRange, - useMacrostratColumnData, -} from "@macrostrat/column-views"; +import { useMacrostratColumnData } from "../data-provider"; import { useCallback, useMemo } from "react"; import hyper from "@macrostrat/hyper"; import styles from "./base-sample-column.module.sass"; +import { getUnitHeightRange } from "../prepare-units"; +import { ColumnNotes } from "../notes"; const h = hyper.styled(styles); export interface BaseMeasurementsColumnProps { diff --git a/packages/column-views/src/facets/detrital-zircon/index.ts b/packages/column-views/src/facets/detrital-zircon/index.ts index de9dbda3..c63b6bf6 100644 --- a/packages/column-views/src/facets/detrital-zircon/index.ts +++ b/packages/column-views/src/facets/detrital-zircon/index.ts @@ -3,13 +3,13 @@ import { DetritalSeries, usePlotArea, } from "@macrostrat/data-components"; -import { IUnit } from "../../units/types"; +import type { IUnit } from "../../units/types"; import hyper from "@macrostrat/hyper"; import { useDetritalMeasurements, MeasurementInfo } from "./provider"; import { useMemo } from "react"; import styles from "./index.module.sass"; import classNames from "classnames"; -import { BaseMeasurementsColumn } from "@macrostrat/column-views"; +import { BaseMeasurementsColumn } from "../base-sample-column"; const h = hyper.styled(styles); diff --git a/packages/column-views/src/facets/fossils/index.ts b/packages/column-views/src/facets/fossils/index.ts index 27fd028d..127184ec 100644 --- a/packages/column-views/src/facets/fossils/index.ts +++ b/packages/column-views/src/facets/fossils/index.ts @@ -11,9 +11,9 @@ import { Box, useElementSize } from "@macrostrat/ui-components"; import { InternMap } from "d3-array"; import { ColumnAxisType, ColumnSVG } from "@macrostrat/column-components"; import { - useCompositeScale, useMacrostratColumnData, -} from "@macrostrat/column-views"; + useCompositeScale, +} from "../../data-provider"; import { UnitLong } from "@macrostrat/api-types"; import styles from "./index.module.sass"; import { useRef } from "react"; diff --git a/packages/column-views/src/prepare-units/index.ts b/packages/column-views/src/prepare-units/index.ts index ceb5cc25..bc5e2472 100644 --- a/packages/column-views/src/prepare-units/index.ts +++ b/packages/column-views/src/prepare-units/index.ts @@ -22,7 +22,7 @@ import { PreparedColumnData, unitsOverlap, } from "./utils"; -import { SectionInfo } from "@macrostrat/column-views"; +import { SectionInfo } from "./types"; export * from "./utils"; export * from "./types"; diff --git a/packages/column-views/src/units/selection.ts b/packages/column-views/src/units/selection.ts index f87f72d7..fa7a5115 100644 --- a/packages/column-views/src/units/selection.ts +++ b/packages/column-views/src/units/selection.ts @@ -10,14 +10,9 @@ import { RefObject, useRef, useCallback, - MouseEvent, } from "react"; import { createStore, StoreApi, useStore } from "zustand"; import type { RectBounds, IUnit } from "./types"; -import { - getUnitHeightRange, - useMacrostratColumnData, -} from "@macrostrat/column-views"; type UnitSelectDispatch = ( unit: number | BaseUnit | null, diff --git a/packages/svg-map-components/src/canvas-layer.ts b/packages/svg-map-components/src/canvas-layer.ts index 48be9938..d9fa5f08 100644 --- a/packages/svg-map-components/src/canvas-layer.ts +++ b/packages/svg-map-components/src/canvas-layer.ts @@ -20,7 +20,7 @@ const MapCanvasContext = createContext({ class CanvasLayer extends Component { static contextType = MapContext; - context: GlobeCtx; + declare context: GlobeCtx; canvas: RefObject; constructor(props, ctx) { super(props, ctx); diff --git a/packages/svg-map-components/src/drag-interaction.ts b/packages/svg-map-components/src/drag-interaction.ts index a4dd99c4..0cbc5ca0 100644 --- a/packages/svg-map-components/src/drag-interaction.ts +++ b/packages/svg-map-components/src/drag-interaction.ts @@ -29,7 +29,7 @@ interface DraggableOverlayInternalProps extends DraggableOverlayProps { class _DraggableOverlay extends Component { static contextType = MapContext; - context: GlobeCtx; + declare context: GlobeCtx; static defaultProps = { showMousePosition: false, diff --git a/packages/timescale/src/intervals-api.ts b/packages/timescale/src/intervals-api.ts index 5430fbf9..c751968c 100644 --- a/packages/timescale/src/intervals-api.ts +++ b/packages/timescale/src/intervals-api.ts @@ -12,7 +12,7 @@ import { Interval } from "./types"; export async function fetchMacrostratIntervals( baseUrl: string, // Default to ICS timescale - timescaleID?: number = 11, + timescaleID: number = 11, ): Promise { const url = new URL(`${baseUrl}/defs/intervals`); url.searchParams.set("timescale_id", timescaleID.toString()); diff --git a/packages/timescale/src/provider.ts b/packages/timescale/src/provider.ts index 5d014703..eb49c3c9 100644 --- a/packages/timescale/src/provider.ts +++ b/packages/timescale/src/provider.ts @@ -6,7 +6,7 @@ import { TimescaleOrientation, IncreaseDirection } from "./types"; const TimescaleContext = createContext(null); -interface TimescaleProviderProps extends TimescaleCTX { +export interface TimescaleProviderProps extends TimescaleCTX { children: ReactNode; increaseDirection?: IncreaseDirection; } From 168f62024af84dfba6c9ba320b5e2b9644d2ad40 Mon Sep 17 00:00:00 2001 From: Daven Quinn Date: Fri, 28 Nov 2025 19:36:03 -0600 Subject: [PATCH 46/46] Fixed @macrostrat/data-components --- .../src/location-info/index.ts | 5 +++- .../src/location-info/utils.ts | 24 ------------------- 2 files changed, 4 insertions(+), 25 deletions(-) delete mode 100644 packages/data-components/src/location-info/utils.ts diff --git a/packages/data-components/src/location-info/index.ts b/packages/data-components/src/location-info/index.ts index 3903bad7..9ac8081b 100644 --- a/packages/data-components/src/location-info/index.ts +++ b/packages/data-components/src/location-info/index.ts @@ -4,7 +4,6 @@ import { metersToFeet, normalizeLng, } from "@macrostrat/mapbox-utils"; -import { formatValue } from "./utils"; import type { LngLatLike } from "mapbox-gl"; export function ValueWithUnit(props) { @@ -16,6 +15,10 @@ export function ValueWithUnit(props) { ]); } +function formatValue(val: number, precision: number = 0): string { + return Number(val).toFixed(precision); +} + export function DegreeCoord(props) { const { value, labels, precision = 3, format = formatValue } = props; const direction = value < 0 ? labels[1] : labels[0]; diff --git a/packages/data-components/src/location-info/utils.ts b/packages/data-components/src/location-info/utils.ts deleted file mode 100644 index 0558f91e..00000000 --- a/packages/data-components/src/location-info/utils.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { format } from "d3-format"; - -export const fmt4 = format(".4~f"); -export const fmt3 = format(".3~f"); -export const fmt2 = format(".2~f"); -export const fmt1 = format(".1~f"); -export const fmtInt = format(".0f"); - -export function formatValue(val: number, precision: number = 0): string { - switch (precision) { - case 4: - return fmt4(val); - case 3: - return fmt3(val); - case 2: - return fmt2(val); - case 1: - return fmt1(val); - case 0: - return fmtInt(val); - default: - return fmt4(val); - } -}