diff --git a/packages/column-views/CHANGELOG.md b/packages/column-views/CHANGELOG.md index e1dd72cc..e2ccad84 100644 --- a/packages/column-views/CHANGELOG.md +++ b/packages/column-views/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). +## [2.2.1] - 2025-11-29 + +- Start unifying state management components +- Create a hoistable store for column state +- Begin using `jotai` for some aspects of state management + ## [2.2.0] - 2025-11-28 - Update SGP and PBDB facets diff --git a/packages/column-views/package.json b/packages/column-views/package.json index 5412996e..4eff05b5 100644 --- a/packages/column-views/package.json +++ b/packages/column-views/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/column-views", - "version": "2.2.0", + "version": "2.2.1", "description": "Data views for Macrostrat stratigraphic columns", "type": "module", "source": "src/index.ts", @@ -63,6 +63,8 @@ "d3-path": "^3.1.0", "d3-scale": "^4.0.2", "d3-shape": "^3.2.0", + "jotai": "^2.15.1", + "jotai-scope": "^0.9.7", "mapbox-gl": "^2.15.0||^3.0.0", "react-spring": "^9.7.5", "topojson-client": "^3.1.0", diff --git a/packages/column-views/src/column.ts b/packages/column-views/src/column.ts index f38c618e..99027a60 100644 --- a/packages/column-views/src/column.ts +++ b/packages/column-views/src/column.ts @@ -16,12 +16,12 @@ import { ComponentType, } from "react"; import styles from "./column.module.sass"; +import { UnitComponent } from "./units"; import { UnitSelectionProvider, - UnitComponent, UnitKeyboardNavigation, useUnitSelectionDispatch, -} from "./units"; +} from "./data-provider"; import { Identifier, @@ -95,7 +95,6 @@ export function Column(props: ColumnProps) { selectedUnit, children, units: rawUnits, - axisType = ColumnAxisType.AGE, t_age, b_age, t_pos, @@ -109,6 +108,7 @@ export function Column(props: ColumnProps) { allowUnitSelection, hybridScale, scale, + axisType, ...rest } = props; const ref = useRef(); @@ -125,8 +125,20 @@ export function Column(props: ColumnProps) { _minPixelScale = pixelScale; } + // Handle special cases for hybrid scales (WIP, we need to regularize this) + let _axisType = axisType ?? ColumnAxisType.AGE; + 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; + _axisType = ColumnAxisType.AGE; + } + const { sections, units, totalHeight } = usePreparedColumnUnits(rawUnits, { - axisType, + axisType: _axisType, t_age, b_age, t_pos, @@ -154,15 +166,6 @@ export function Column(props: ColumnProps) { ); } - 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 }, @@ -196,7 +199,7 @@ export function Column(props: ColumnProps) { return h( MacrostratColumnDataProvider, - { units, sections, totalHeight, axisType }, + { units, sections, totalHeight, axisType: _axisType }, main, ); } diff --git a/packages/column-views/src/correlation-chart/main.ts b/packages/column-views/src/correlation-chart/main.ts index 78ac315b..c4d4f26d 100644 --- a/packages/column-views/src/correlation-chart/main.ts +++ b/packages/column-views/src/correlation-chart/main.ts @@ -3,7 +3,7 @@ import { UnitSelectionProvider, UnitKeyboardNavigation, useUnitSelectionStore, -} from "../units"; +} from "../data-provider"; import { UnitSelectionPopover } from "../unit-details"; import hyper from "@macrostrat/hyper"; import styles from "./main.module.sass"; diff --git a/packages/column-views/src/data-provider/index.ts b/packages/column-views/src/data-provider/index.ts index a31d39bf..fda8a356 100644 --- a/packages/column-views/src/data-provider/index.ts +++ b/packages/column-views/src/data-provider/index.ts @@ -2,3 +2,4 @@ export * from "./fetch"; export * from "./base"; export * from "./column-nav"; export * from "./store"; +export * from "./unit-selection"; diff --git a/packages/column-views/src/data-provider/store.ts b/packages/column-views/src/data-provider/store.ts index 846d06bf..b0f79ba0 100644 --- a/packages/column-views/src/data-provider/store.ts +++ b/packages/column-views/src/data-provider/store.ts @@ -6,6 +6,61 @@ import { } from "../prepare-units/composite-scale"; import { ColumnAxisType } from "@macrostrat/column-components"; 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"; + +const { Provider, useAtom, useAtomValue, useStore } = createIsolation(); + +type ProviderProps = { + children: ReactNode; + initialValues?: Iterable<[WritableAtom, any]>; +}; + +const columnUnitsAtom = atom(); + +const columnUnitsMapAtom = atom | null>((get) => { + const units = get(columnUnitsAtom); + if (!units) return null; + const unitMap = new Map(); + units.forEach((unit) => { + unitMap.set(unit.unit_id, unit); + }); + return unitMap; +}); + +function ScopedProvider({ children, ...rest }: ProviderProps) { + // Always use the same store instance in this tree + let val = null; + try { + val = useStore(); + } catch { + // No store found, create a new one + val = null; + } + return h(Provider, { store: val, ...rest }, children); +} + +export function MacrostratColumnStateProvider({ + children, + units, +}: { + children: ReactNode; + units: ExtUnit[]; +}) { + /** Top-level provider for Macrostrat column data. + * It is either provided by the Column component itself, or + * can be hoisted higher in the tree to provide a common data context + */ + return h( + ScopedProvider, + { + initialValues: [[columnUnitsAtom, units]], + }, + children, + ); +} export interface MacrostratColumnDataContext { units: ExtUnit[]; @@ -24,7 +79,7 @@ export function MacrostratColumnDataProvider({ totalHeight, axisType, }: MacrostratColumnDataContext & { children: ReactNode }) { - /** Provider for Macrostrat column data. + /** Internal provider for Macrostrat column data. * As a general rule, we want to provide data and column-axis * height calculations through the context, since these need to * be accessed by any component that lays out information on the @@ -45,7 +100,11 @@ export function MacrostratColumnDataProvider({ }; }, [units, sections, totalHeight, axisType]); - return h(MacrostratColumnDataContext.Provider, { value }, children); + return h( + MacrostratColumnStateProvider, + { units }, + h(MacrostratColumnDataContext.Provider, { value }, children), + ); } export function useMacrostratColumnData() { @@ -59,19 +118,15 @@ export function useMacrostratColumnData() { } export function useMacrostratUnits() { - return useMacrostratColumnData().units; + return useAtomValue(columnUnitsAtom); } 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]); + try { + return useAtomValue(columnUnitsMapAtom); + } catch { + return null; + } } export function useCompositeScale(): CompositeColumnScale { diff --git a/packages/column-views/src/units/selection.ts b/packages/column-views/src/data-provider/unit-selection.ts similarity index 99% rename from packages/column-views/src/units/selection.ts rename to packages/column-views/src/data-provider/unit-selection.ts index fa7a5115..d98cfd83 100644 --- a/packages/column-views/src/units/selection.ts +++ b/packages/column-views/src/data-provider/unit-selection.ts @@ -12,7 +12,7 @@ import { useCallback, } from "react"; import { createStore, StoreApi, useStore } from "zustand"; -import type { RectBounds, IUnit } from "./types"; +import type { RectBounds, IUnit } from "../units/types"; type UnitSelectDispatch = ( unit: number | BaseUnit | null, diff --git a/packages/column-views/src/maps/column-correlation/state.ts b/packages/column-views/src/maps/column-correlation/state.ts index 2855cd33..55b2263e 100644 --- a/packages/column-views/src/maps/column-correlation/state.ts +++ b/packages/column-views/src/maps/column-correlation/state.ts @@ -6,7 +6,6 @@ import { lineIntersect } from "@turf/line-intersect"; import distance from "@turf/distance"; import { nearestPointOnLine } from "@turf/nearest-point-on-line"; import { centroid } from "@turf/centroid"; -import mapboxgl from "mapbox-gl"; import { createContext, useState, diff --git a/packages/column-views/src/unit-details/panel.ts b/packages/column-views/src/unit-details/panel.ts index ef86bd05..e3d1d477 100644 --- a/packages/column-views/src/unit-details/panel.ts +++ b/packages/column-views/src/unit-details/panel.ts @@ -593,7 +593,7 @@ function UnitIDList({ units, onSelectUnit, showNames = false }) { const u1 = units.filter((d) => d != 0); if (showNames) { return u1.map((unitID) => { - const unitData = unitsMap.get(unitID); + const unitData = unitsMap?.get(unitID); let name: string = undefined; if (unitData != null) { name = defaultNameFunction(unitData); diff --git a/packages/column-views/src/unit-details/popover.ts b/packages/column-views/src/unit-details/popover.ts index 60c551c0..5bdcfd36 100644 --- a/packages/column-views/src/unit-details/popover.ts +++ b/packages/column-views/src/unit-details/popover.ts @@ -1,7 +1,7 @@ import hyper from "@macrostrat/hyper"; import { Popover } from "@blueprintjs/core"; import styles from "./popover.module.sass"; -import { useSelectedUnit, useUnitSelectionStore } from "../units"; +import { useSelectedUnit, useUnitSelectionStore } from "../data-provider"; import { UnitDetailsPanel } from "./panel"; const h = hyper.styled(styles); diff --git a/packages/column-views/src/units/boxes.ts b/packages/column-views/src/units/boxes.ts index e42ccd1f..d3715623 100644 --- a/packages/column-views/src/units/boxes.ts +++ b/packages/column-views/src/units/boxes.ts @@ -12,12 +12,11 @@ import { SizeAwareLabel, Clickable } from "@macrostrat/ui-components"; import hyper from "@macrostrat/hyper"; import { forwardRef, ReactNode, useContext, useMemo } from "react"; import { resolveID, scalePattern } from "./resolvers"; -import { useUnitSelectionTarget } from "./selection"; +import { useUnitSelectionTarget, useLithologies } from "../data-provider"; import { IUnit } from "./types"; import styles from "./boxes.module.sass"; import classNames from "classnames"; import { getUnitHeightRange } from "../prepare-units/utils"; -import { useLithologies } from "../data-provider"; import { getMixedUnitColor } from "./colors"; import type { RectBounds } from "./types"; diff --git a/packages/column-views/src/units/composite.ts b/packages/column-views/src/units/composite.ts index b5f35d17..09e8b976 100644 --- a/packages/column-views/src/units/composite.ts +++ b/packages/column-views/src/units/composite.ts @@ -16,8 +16,11 @@ import { import { BaseUnit } from "@macrostrat/api-types"; import { LabeledUnit, UnitBoxes } from "./boxes"; import styles from "./composite.module.sass"; -import { useCompositeScale, useMacrostratColumnData } from "../data-provider"; -import { useUnitSelectionDispatch } from "../units"; +import { + useCompositeScale, + useMacrostratColumnData, + useUnitSelectionDispatch, +} from "../data-provider"; const h = hyperStyled(styles); diff --git a/packages/column-views/src/units/index.ts b/packages/column-views/src/units/index.ts index cb00d41f..5c0e984b 100644 --- a/packages/column-views/src/units/index.ts +++ b/packages/column-views/src/units/index.ts @@ -17,7 +17,6 @@ import { Lithology } from "@macrostrat/api-types"; export * from "./composite"; export * from "./types"; -export * from "./selection"; export * from "./colors"; export function UnitsColumn({ width = 100 }) { diff --git a/packages/column-views/stories/nonlinear-scale.stories.ts b/packages/column-views/stories/nonlinear-scale.stories.ts index 2d7cd384..9a27f709 100644 --- a/packages/column-views/stories/nonlinear-scale.stories.ts +++ b/packages/column-views/stories/nonlinear-scale.stories.ts @@ -167,7 +167,7 @@ export const WithApproximateHeightScaleOnly: Story = { id: 448, // Ordered time bins pixelScale: 0.5, - axisType: ColumnAxisType.AGE, + axisType: ColumnAxisType.HEIGHT, hybridScale: { type: HybridScaleType.ApproximateHeight, }, @@ -177,9 +177,5 @@ export const WithApproximateHeightScaleOnly: Story = { showTimescale: false, unconformityHeight: 20, unconformityLabels: false, - children: h(ApproximateHeightAxis, { - // Move to the left side - style: { order: -1, marginRight: "8px" }, - }), }, }; diff --git a/packages/column-views/stories/state-isolation.stories.ts b/packages/column-views/stories/state-isolation.stories.ts new file mode 100644 index 00000000..4bdf4ae7 --- /dev/null +++ b/packages/column-views/stories/state-isolation.stories.ts @@ -0,0 +1,78 @@ +import { createIsolation } from "jotai-scope"; +import { atom } from "jotai"; +import h from "@macrostrat/hyper"; +import { Store } from "jotai/vanilla/store"; +import { ComponentProps } from "react"; + +const { Provider, useAtom, useStore } = createIsolation(); + +const countAtom = atom(0); + +export default { + title: "Column views/State isolation", + description: + "Example of isolating state between different Jotai provider instances.", +}; + +export function StateIsolationExample() { + return h("div", [ + h(InheritProvider, { initialValues: [[countAtom, 5]] }, [ + h(Counter, { key: "counter-1" }), + h(InheritProvider, { initialValues: [[countAtom, 8]] }, [ + h("p", [ + "This should inherit from the outer provider, if available. The initial value is ignored", + h(Counter, { key: "counter-2" }), + ]), + ]), + h("hr"), + ]), + h(InheritProvider, { initialValues: [[countAtom, 10]] }, [ + h("p", [ + "This provider is separate because it is not nested. ", + h(Counter, { key: "counter-3" }), + ]), + ]), + ]); +} + +type InheritProps = ComponentProps; + +function InheritProvider({ children, ...rest }: InheritProps) { + let store: Store | null; + try { + store = useStore(); + } catch { + store = null; + } + return h(Provider, { store, ...rest }, children); +} + +function Counter() { + const [count, setCount] = useAtom(countAtom); + return h("div", [ + h("p", [ + `Count: ${count}`, + " ", + h( + "button", + { + onClick: () => setCount((c) => c + 1), + }, + "Increment", + ), + " ", + h(Reset), + ]), + ]); +} + +function Reset() { + const [, setCount] = useAtom(countAtom); + return h( + "button", + { + onClick: () => setCount(0), + }, + "Reset Count", + ); +} diff --git a/packages/column-views/stories/unit-details.stories.ts b/packages/column-views/stories/unit-details.stories.ts index 3792a02f..c2fd8cef 100644 --- a/packages/column-views/stories/unit-details.stories.ts +++ b/packages/column-views/stories/unit-details.stories.ts @@ -5,7 +5,14 @@ import { useAPIResult } from "@macrostrat/ui-components"; import { Button, Spinner } from "@blueprintjs/core"; import "@macrostrat/style-system"; import { UnitDetailsPanel } from "../src/unit-details"; -import { LithologiesProvider } from "../src"; +import { + LithologiesProvider, + MacrostratColumnStateProvider, + UnitSelectionProvider, + useSelectedUnit, + useUnitSelectionStore, +} from "../src"; +import { useColumnUnits } from "./column-ui/utils"; function useUnitData(unit_id, inProcess = false) { return useAPIResult( @@ -105,7 +112,6 @@ export const MoenkopiFormation: Story = { export const eODPMudstone: Story = { args: { unit_id: 62623, - inProcess: true, }, }; @@ -123,3 +129,31 @@ export const WithActions: Story = { ]), }, }; + +export function WithDataProvider(args: UnitDetailsProps) { + const units = useColumnUnits(432); + + if (units == null) return h(Spinner); + + return h( + MacrostratColumnStateProvider, + { units }, + h( + UnitSelectionProvider, + { units, selectedUnit: units?.[0]?.unit_id }, + h(UnitDetailsWithSelection), + ), + ); +} + +function UnitDetailsWithSelection(args: UnitDetailsProps) { + const unit = useSelectedUnit(); + const setSelectedUnit = useUnitSelectionStore((s) => s.onUnitSelected); + if (unit == null) return h("div", "No unit selected"); + return h(UnitDetailsPanel, { + unit, + onSelectUnit(unit) { + setSelectedUnit(unit, null, null); + }, + }); +} diff --git a/packages/svg-map-components/CHANGELOG.md b/packages/svg-map-components/CHANGELOG.md index e6e3a4f4..09096d3b 100644 --- a/packages/svg-map-components/CHANGELOG.md +++ b/packages/svg-map-components/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [1.0.7] - 2025-11-29 + +Small update to types. + ## [1.0.6] - 2025-10-29 Remove use of `findDOMNode` diff --git a/packages/svg-map-components/package.json b/packages/svg-map-components/package.json index eab3b76d..82964e1a 100644 --- a/packages/svg-map-components/package.json +++ b/packages/svg-map-components/package.json @@ -1,6 +1,6 @@ { "name": "@macrostrat/svg-map-components", - "version": "1.0.6", + "version": "1.0.7", "description": "React components for vector maps", "keywords": [ "gis", diff --git a/yarn.lock b/yarn.lock index 78cd618e..13055aac 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2343,6 +2343,8 @@ __metadata: d3-path: "npm:^3.1.0" d3-scale: "npm:^4.0.2" d3-shape: "npm:^3.2.0" + jotai: "npm:^2.15.1" + jotai-scope: "npm:^0.9.7" mapbox-gl: "npm:^2.15.0||^3.0.0" parcel: "npm:^2.16.0" postcss: "npm:^8.0.0" @@ -9266,6 +9268,37 @@ __metadata: languageName: node linkType: hard +"jotai-scope@npm:^0.9.7": + version: 0.9.7 + resolution: "jotai-scope@npm:0.9.7" + peerDependencies: + jotai: ">=2.15.0" + react: ">=16.0.0" + checksum: 10c0/7ecaa7ce55ae51e6253450956eb2764148393883b16d7c254b9fea13931d38dc3875d2be8082271cba350d7f7915f08fa57e9164d9b4f3e076a581a201009137 + languageName: node + linkType: hard + +"jotai@npm:^2.15.1": + version: 2.15.1 + resolution: "jotai@npm:2.15.1" + peerDependencies: + "@babel/core": ">=7.0.0" + "@babel/template": ">=7.0.0" + "@types/react": ">=17.0.0" + react: ">=17.0.0" + peerDependenciesMeta: + "@babel/core": + optional: true + "@babel/template": + optional: true + "@types/react": + optional: true + react: + optional: true + checksum: 10c0/8260ac9f36e14d2cedbc79f63bc1bcd82902d68a1ab1f3dfce6ffabccbedfd5fa54fffe93e53a09253d6b29a9c05fbc455108a280efc8d349fd5bfb965f7869b + languageName: node + linkType: hard + "js-tokens@npm:^3.0.0 || ^4.0.0, js-tokens@npm:^4.0.0": version: 4.0.0 resolution: "js-tokens@npm:4.0.0"