diff --git a/packages/column-views/CHANGELOG.md b/packages/column-views/CHANGELOG.md index e2ccad84..41d4811c 100644 --- a/packages/column-views/CHANGELOG.md +++ b/packages/column-views/CHANGELOG.md @@ -4,6 +4,14 @@ 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.2] - 2025-12-04 + +- Fix a bug with unit deselection +- Fix missed updates in state management code +- Add a 'minimal' option to `unconformityLabels` +- Reduce precision of gap age labels +- Improvements to stories + ## [2.2.1] - 2025-11-29 - Start unifying state management components diff --git a/packages/column-views/package.json b/packages/column-views/package.json index 4eff05b5..8c484545 100644 --- a/packages/column-views/package.json +++ b/packages/column-views/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/column-views", - "version": "2.2.1", + "version": "2.2.2", "description": "Data views for Macrostrat stratigraphic columns", "type": "module", "source": "src/index.ts", diff --git a/packages/column-views/src/column.module.sass b/packages/column-views/src/column.module.sass index 6f5a52fe..7297e892 100644 --- a/packages/column-views/src/column.module.sass +++ b/packages/column-views/src/column.module.sass @@ -137,12 +137,11 @@ body:global(.dark-mode) .column-container --unit-font-weight: 400 .unconformity-inner - //border-left: 1.5px dotted var(--secondary-color) position: absolute - top: 5px - bottom: 5px - left: -3px - right: -2px + top: 3px + bottom: 3px + left: 0 + right: 0 display: flex justify-content: center align-items: center @@ -155,6 +154,9 @@ body:global(.dark-mode) .column-container font-weight: 400 color: var(--secondary-color) +.timescale-column .unconformity-inner + border-left: 1.5px dotted var(--secondary-color) + left: -3px // align with age axis .column-title-row diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index 99027a60..8e0f149c 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -61,6 +61,7 @@ interface BaseColumnProps extends SectionSharedProps { // Timescale properties showTimescale?: boolean; timescaleLevels?: number | [number, number]; + unconformityLabels?: boolean | UnconformityLabelPlacement; onMouseOver?: ( unit: UnitLong | null, height: number | null, @@ -68,6 +69,8 @@ interface BaseColumnProps extends SectionSharedProps { ) => void; } +export type UnconformityLabelPlacement = "minimal" | "prominent" | "none"; + export interface ColumnProps extends Padding, BaseColumnProps, ColumnHeightScaleOptions { // Macrostrat units @@ -222,7 +225,7 @@ function ColumnInner(props: ColumnInnerProps) { const { unitComponent = UnitComponent, - unconformityLabels = true, + unconformityLabels = "minimal", showLabels = true, width: _width = 300, columnWidth: _columnWidth = 150, @@ -240,6 +243,15 @@ function ColumnInner(props: ColumnInnerProps) { const { axisType } = useMacrostratColumnData(); + // Coalesce unconformity label setting to a boolean + let _timescaleUnconformityLabels = false; + let _sectionUnconformityLabels = false; + if (unconformityLabels === true || unconformityLabels === "prominent") { + _sectionUnconformityLabels = true; + } else if (unconformityLabels === "minimal") { + _timescaleUnconformityLabels = true; + } + let width = _width; let columnWidth = _columnWidth; if (columnWidth > width) { @@ -265,7 +277,10 @@ function ColumnInner(props: ColumnInnerProps) { }, h("div.column", { ref: columnRef }, [ h(ageAxisComponent), - h.if(_showTimescale)(CompositeTimescale, { levels: timescaleLevels }), + h.if(_showTimescale)(CompositeTimescale, { + levels: timescaleLevels, + unconformityLabels: _timescaleUnconformityLabels, + }), h(SectionsColumn, { unitComponent, showLabels, @@ -273,7 +288,7 @@ function ColumnInner(props: ColumnInnerProps) { columnWidth, showLabelColumn, clipUnits, - unconformityLabels, + unconformityLabels: _sectionUnconformityLabels, maxInternalColumns, }), children, @@ -351,7 +366,11 @@ export function ColumnContainer(props: ColumnContainerProps) { }); } -export function ColumnBasicInfo({ data, showColumnID = true }) { +export function ColumnBasicInfo({ + data, + showColumnID = true, + showReferences = true, +}) { if (data == null) return null; return h("div.column-info", [ h("div.column-title-row", [ @@ -359,10 +378,10 @@ export function ColumnBasicInfo({ data, showColumnID = true }) { h.if(showColumnID)("h4", h(Identifier, { id: data.col_id })), ]), h(DataField, { row: true, label: "Group", value: data.col_group }), - h(ReferencesField, { + h.if(showReferences)(ReferencesField, { refs: data.refs, inline: false, - row: true, + row: false, className: "column-refs", }), ]); diff --git a/packages/column-views/src/data-provider/store.ts b/packages/column-views/src/data-provider/store.ts index b0f79ba0..b9db0b77 100644 --- a/packages/column-views/src/data-provider/store.ts +++ b/packages/column-views/src/data-provider/store.ts @@ -1,4 +1,11 @@ -import { createContext, ReactNode, useContext, useMemo } from "react"; +import { + createContext, + ReactNode, + useContext, + useEffect, + useMemo, + useRef, +} from "react"; import h from "@macrostrat/hyper"; import { CompositeColumnScale, @@ -9,9 +16,9 @@ import type { ExtUnit, PackageLayoutData } from "../prepare-units"; // An isolated jotai store for Macrostrat column usage // TODO: there might be a better way to do this using the MacrostratDataProvider or similar import { createIsolation } from "jotai-scope"; -import { atom, type WritableAtom } from "jotai"; +import { atom, PrimitiveAtom, type WritableAtom } from "jotai"; -const { Provider, useAtom, useAtomValue, useStore } = createIsolation(); +const { Provider, useSetAtom, useAtomValue, useStore } = createIsolation(); type ProviderProps = { children: ReactNode; @@ -53,12 +60,17 @@ export function MacrostratColumnStateProvider({ * It is either provided by the Column component itself, or * can be hoisted higher in the tree to provide a common data context */ + + const atomMap: [[PrimitiveAtom, ExtUnit[]]] = [ + [columnUnitsAtom, units], + ]; + return h( ScopedProvider, { - initialValues: [[columnUnitsAtom, units]], + initialValues: atomMap, }, - children, + [h(AtomUpdater, { atoms: atomMap }), children], ); } @@ -136,3 +148,34 @@ export function useCompositeScale(): CompositeColumnScale { [ctx.sections], ); } + +function AtomUpdater({ + atoms, +}: { + atoms: [WritableAtom, any][]; +}) { + /** + * A generic updater to sync Jotai atoms with state passed as props. + * Useful for scoped providers where state needs to be synced outside + * of the current context. + */ + /** TODO: this is an awkward way to keep atoms updated */ + // The scoped store + const store = useStore(); + + // Warn on atoms changing length + const prevLengthRef = useRef(atoms.length); + useEffect(() => { + if (prevLengthRef.current !== atoms.length) { + console.error("Error: number of atoms in ScopedAtomUpdater has changed."); + prevLengthRef.current = atoms.length; + } + }, [atoms.length]); + + for (const [atom, value] of atoms) { + useEffect(() => { + store.set(atom, value); + }, [store, value]); + } + return null; +} diff --git a/packages/column-views/src/data-provider/unit-selection.ts b/packages/column-views/src/data-provider/unit-selection.ts index d98cfd83..7d062d09 100644 --- a/packages/column-views/src/data-provider/unit-selection.ts +++ b/packages/column-views/src/data-provider/unit-selection.ts @@ -93,6 +93,7 @@ export function UnitSelectionProvider(props: { event: PointerEvent = null, ) => { if (input == null) { + props.onUnitSelected?.(null, null); return set({ selectedUnit: null, selectedUnitData: null, diff --git a/packages/column-views/src/section.ts b/packages/column-views/src/section.ts index e97325a7..31ad95f3 100644 --- a/packages/column-views/src/section.ts +++ b/packages/column-views/src/section.ts @@ -63,7 +63,7 @@ export function SectionsColumn(props: SectionSharedProps) { const units = useMacrostratUnits(); // Get a unique key for the column - const key = units[0]?.unit_id; + const key = units[0]?.col_id; return h(LabelTrackerProvider, { units, key }, [ h(SectionUnitsColumn, { @@ -199,6 +199,7 @@ function SectionUnits(props: SectionProps) { interface CompositeTimescaleProps { levels?: [number, number] | number; + unconformityLabels?: boolean; } export function CompositeTimescale(props: CompositeTimescaleProps) { @@ -215,7 +216,6 @@ export function CompositeTimescale(props: CompositeTimescaleProps) { type CompositeTimescaleCoreProps = CompositeTimescaleProps & { packages: PackageScaleLayoutData[]; - unconformityLabels?: boolean; }; export function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) { @@ -316,6 +316,8 @@ function Unconformity({ const ageGap = Math.abs(upperAge - lowerAge); + let maximumFractionDigits = 0; + let className: string = null; if (ageGap > 1000) { className = "giga"; @@ -325,14 +327,17 @@ function Unconformity({ className = "large"; } else if (ageGap < 1) { className = "small"; + maximumFractionDigits = 2; + } else { + maximumFractionDigits = 1; } let val: ReactNode; if (axisType === ColumnAxisType.DEPTH || axisType === ColumnAxisType.HEIGHT) { - const _txt = ageGap.toLocaleString("en-US", { maximumFractionDigits: 2 }); + const _txt = ageGap.toLocaleString("en-US", { maximumFractionDigits }); val = h(Value, { value: _txt, unit: "m" }); } else { - val = h(Duration, { value: ageGap }); + val = h(Duration, { value: ageGap, maximumFractionDigits }); } let prefix: ReactNode = null; diff --git a/packages/column-views/src/unit-details/panel.ts b/packages/column-views/src/unit-details/panel.ts index e3d1d477..5a713c3f 100644 --- a/packages/column-views/src/unit-details/panel.ts +++ b/packages/column-views/src/unit-details/panel.ts @@ -301,10 +301,11 @@ export function ReferencesField({ refs, className = null, ...rest }) { if (refs == null || refs.length === 0) { return null; } + return h( DataField, { - label: "Source", + label: "References", className: classNames("refs-field", className), ...rest, }, diff --git a/packages/column-views/stories/arg-types.ts b/packages/column-views/stories/arg-types.ts new file mode 100644 index 00000000..87a90349 --- /dev/null +++ b/packages/column-views/stories/arg-types.ts @@ -0,0 +1,64 @@ +export const sharedColumnArgTypes = { + columnID: { + control: { + type: "number", + }, + }, + selectedUnit: { + control: { + type: "number", + }, + }, + t_age: { + control: { + type: "number", + }, + }, + b_age: { + control: { + type: "number", + }, + }, + unconformityLabels: { + options: ["minimal", "prominent", "none"], + control: { type: "radio" }, + }, + axisType: { + options: ["age", "ordinal", "depth"], + control: { type: "radio" }, + }, + mergeSections: { + options: ["all", "overlapping", null], + control: { type: "radio" }, + }, + pixelScale: { + control: { + type: "number", + }, + }, + collapseSmallUnconformities: { + control: { + type: "boolean", + }, + }, + minSectionHeight: { + control: { + type: "number", + }, + }, + targetUnitHeight: { + control: { + type: "number", + }, + }, + showLabelColumn: { + control: { + type: "boolean", + }, + }, + maxInternalColumns: { + control: { + type: "number", + }, + }, +}; diff --git a/packages/column-views/stories/column-navigation.stories.ts b/packages/column-views/stories/column-navigation.stories.ts index 17d336ae..f5329c74 100644 --- a/packages/column-views/stories/column-navigation.stories.ts +++ b/packages/column-views/stories/column-navigation.stories.ts @@ -9,32 +9,10 @@ import { EnvironmentColoredUnitComponent, } from "../src"; import { useColumnSelection } from "./column-ui/utils"; - -const baseArgTypes = { - columnID: { - control: { - type: "number", - }, - }, - selectedUnit: { - control: { - type: "number", - }, - }, - t_age: { - control: { - type: "number", - }, - }, - b_age: { - control: { - type: "number", - }, - }, -}; +import { sharedColumnArgTypes } from "./arg-types"; export default { - title: "Column views/Stratigraphic columns", + title: "Column views/Column navigation", component: ColumnStoryUI, args: { columnID: 432, @@ -43,45 +21,7 @@ export default { targetUnitHeight: 20, }, argTypes: { - ...baseArgTypes, - axisType: { - options: ["age", "ordinal", "depth"], - control: { type: "radio" }, - }, - mergeSections: { - options: ["all", "overlapping", null], - control: { type: "radio" }, - }, - pixelScale: { - control: { - type: "number", - }, - }, - collapseSmallUnconformities: { - control: { - type: "boolean", - }, - }, - minSectionHeight: { - control: { - type: "number", - }, - }, - targetUnitHeight: { - control: { - type: "number", - }, - }, - showLabelColumn: { - control: { - type: "boolean", - }, - }, - maxInternalColumns: { - control: { - type: "number", - }, - }, + ...sharedColumnArgTypes, }, } as Meta; @@ -115,7 +55,6 @@ Minimal.args = { export const eODP = Template.bind({}); eODP.args = { columnID: 5576, - inProcess: true, axisType: "depth", projectID: 3, pixelScale: undefined, diff --git a/packages/column-views/stories/column-page.stories.ts b/packages/column-views/stories/column-page.stories.ts index 9a9a60f0..ead9e2a7 100644 --- a/packages/column-views/stories/column-page.stories.ts +++ b/packages/column-views/stories/column-page.stories.ts @@ -8,15 +8,13 @@ import { UnitDetailsPanelWithNavigation, ReferencesField, UnitDetailsFeature, - Identifier, ColumnBasicInfo, } from "../src"; import { useColumnBasicInfo, useColumnUnits } from "./column-ui/utils"; import styles from "./column-page.stories.module.sass"; import { UnitLong } from "@macrostrat/api-types"; import { useArgs } from "storybook/preview-api"; -import { DataField } from "@macrostrat/data-components"; -import { FlexRow } from "@macrostrat/ui-components"; +import { sharedColumnArgTypes } from "./arg-types"; export default { title: "Column views/Column page", @@ -24,6 +22,10 @@ export default { args: { columnID: 494, selectedUnitID: 15160, + unconformityLabels: "minimal", + }, + argTypes: { + ...sharedColumnArgTypes, }, } as Meta; @@ -57,13 +59,11 @@ function ColumnStoryUI({ return h("div.column-ui", [ h("div.column-container", [ - h(ColumnBasicInfo, { data: info }), + h(ColumnBasicInfo, { data: info, showReferences: false }), h(Column, { - key: columnID, units, selectedUnit: selectedUnitID, onUnitSelected: setSelectedUnitID, - unconformityLabels: true, keyboardNavigation: true, columnWidth: 300, showUnitPopover: false, @@ -73,6 +73,12 @@ function ColumnStoryUI({ targetUnitHeight: 20, ...rest, }), + h(ReferencesField, { + refs: info?.refs, + inline: false, + row: false, + className: "column-refs", + }), ]), h("div.right-column", [ h(ColumnNavigationMap, { diff --git a/packages/column-views/stories/column-ui/story-ui.ts b/packages/column-views/stories/column-ui/story-ui.ts index f4a309ea..a0e7555d 100644 --- a/packages/column-views/stories/column-ui/story-ui.ts +++ b/packages/column-views/stories/column-ui/story-ui.ts @@ -2,6 +2,7 @@ import { ColoredUnitComponent, Column, ColumnNavigationMap, + MacrostratDataProvider, } from "@macrostrat/column-views"; import { hyperStyled } from "@macrostrat/hyper"; import styles from "./story-ui.module.sass"; @@ -23,28 +24,31 @@ export function ColumnStoryUI({ projectID, ...rest }) { - return h("div.column-ui", [ - h( - "div.column-container", - h(ColumnCore, { - col_id: columnID, - selectedUnit, - setSelectedUnit, - inProcess, - ...rest, - }), - ), - h("div.right-column", [ - h(ColumnNavigationMap, { - inProcess, - projectID, - accessToken: mapboxToken, - selectedColumn: columnID, - onSelectColumn: setColumn, - className: "column-selector-map", - }), + return h( + MacrostratDataProvider, + h("div.column-ui", [ + h( + "div.column-container", + h(ColumnCore, { + col_id: columnID, + selectedUnit, + setSelectedUnit, + inProcess, + ...rest, + }), + ), + h("div.right-column", [ + h(ColumnNavigationMap, { + inProcess, + projectID, + accessToken: mapboxToken, + selectedColumn: columnID, + onSelectColumn: setColumn, + className: "column-selector-map", + }), + ]), ]), - ]); + ); } function ColumnCore({ @@ -64,7 +68,6 @@ function ColumnCore({ return h("div.column-container", [ h("h2", info.col_name), h(Column, { - key: col_id, units, selectedUnit, onUnitSelected: (unit_id) => { diff --git a/packages/column-views/stories/column-ui/utils.ts b/packages/column-views/stories/column-ui/utils.ts index 03b49058..54ffcdff 100644 --- a/packages/column-views/stories/column-ui/utils.ts +++ b/packages/column-views/stories/column-ui/utils.ts @@ -2,31 +2,26 @@ import { useAPIResult } from "@macrostrat/ui-components"; import { useMemo, useCallback } from "react"; import { BaseUnit } from "@macrostrat/api-types"; import { useArgs } from "storybook/preview-api"; +import { useMacrostratBaseURL } from "@macrostrat/column-views"; export function useColumnUnits(col_id, inProcess = false): BaseUnit[] | null { const status_code = inProcess ? "in process" : undefined; + const baseURL = useMacrostratBaseURL(); const params = useMemo(() => { return { col_id, response: "long", status_code, show_position: true }; }, [col_id, status_code]); - return useAPIResult( - "https://macrostrat.org/api/v2/units", - params, - (res) => res.success.data, - ); + return useAPIResult(baseURL + "/units", params, (res) => res.success.data); } export function useColumnBasicInfo(col_id, inProcess = false) { const status_code = inProcess ? "in process" : undefined; + const baseURL = useMacrostratBaseURL(); const params = useMemo(() => { return { col_id, status_code }; }, [col_id, status_code]); - return useAPIResult( - "https://macrostrat.org/api/v2/columns", - params, - (res) => { - return res.success?.data[0]; - }, - ); + return useAPIResult(baseURL + "/columns", params, (res) => { + return res.success?.data[0]; + }); } export function useColumnSelection() {