UNPKG

@macrostrat/column-components

Version:

React rendering primitives for stratigraphic columns

259 lines (258 loc) 7.64 kB
import { createContext, useContext } from "react"; import { StatefulComponent } from "@macrostrat/ui-components"; import h from "../hyper.js"; import { hasSpan } from "./utils.js"; import { Renderer, FlexibleNode, Node, Force } from "./label-primitives.js"; import { compareAgeRanges, AgeRangeRelationship } from "@macrostrat/stratigraphy-utils"; import { ColumnLayoutProvider } from "../context/layout.js"; import { ColumnContext } from "../context/column.js"; const NoteLayoutContext = createContext(null); const buildColumnIndex = function() { const heightTracker = []; return function(note) { let colIx = 0; const nPossibleCols = heightTracker.length + 1; for (let column = 0, end = nPossibleCols, asc = 0 <= end; asc ? column <= end : column >= end; asc ? column++ : column--) { if (heightTracker[column] == null) { heightTracker[column] = note.height; } if (heightTracker[column] < note.height) { const hy = note.top_height || note.height; heightTracker[column] = hy; colIx = column; break; } } return colIx; }; }; function withinDomain(scale) { const scaleDomain = scale.domain(); const d1 = [ Math.min(...scaleDomain), Math.max(...scaleDomain) ]; return (d) => { const noteRange = [d.height, d.top_height ?? d.height]; const rel = compareAgeRanges(d1, noteRange); return rel !== AgeRangeRelationship.Disjoint; }; } class NoteLayoutProvider extends StatefulComponent { static contextType = ColumnContext; static defaultProps = { paddingLeft: 60, estimatedTextHeight(note, width) { note.note || ""; return 12; } }; _previousContext; _rendererIndex; constructor(props) { super(props); this.computeContextValue = this.computeContextValue.bind(this); this.savedRendererForWidth = this.savedRendererForWidth.bind(this); this.generatePath = this.generatePath.bind(this); this.createNodeForNote = this.createNodeForNote.bind(this); this.computeForceLayout = this.computeForceLayout.bind(this); this.updateHeight = this.updateHeight.bind(this); this.updateNotes = this.updateNotes.bind(this); this.componentDidMount = this.componentDidMount.bind(this); this.componentDidUpdate = this.componentDidUpdate.bind(this); const { noteComponent } = this.props; this.state = { notes: [], elementHeights: {}, columnIndex: {}, nodes: {}, generatePath: this.generatePath, createNodeForNote: this.createNodeForNote, noteComponent }; } render() { const { children, width } = this.props; return h( NoteLayoutContext.Provider, { value: this.state }, h(ColumnLayoutProvider, { width }, children) ); } computeContextValue() { const { width, paddingLeft } = this.props; const { scaleClamped: scale } = this.context; const forwardedValues = { // Forwarded values from column context // There may be a more elegant way to do this paddingLeft, scale, width }; const renderer = new Renderer({ direction: "right", layerGap: paddingLeft, nodeHeight: 5 }); return this.setState({ renderer, updateHeight: this.updateHeight, generatePath: this.generatePath, ...forwardedValues }); } savedRendererForWidth(width) { if (this._rendererIndex == null) { this._rendererIndex = {}; } if (this._rendererIndex[width] == null) { this._rendererIndex[width] = new Renderer({ direction: "right", layerGap: width, nodeHeight: 5 }); } return this._rendererIndex[width]; } generatePath(node, pixelOffset) { const { paddingLeft } = this.props; const renderer = this.savedRendererForWidth(paddingLeft - pixelOffset); try { return renderer.generatePath(node); } catch (err) { return null; } } createNodeForNote(note) { const { notes, elementHeights } = this.state; let { scaleClamped: scale } = this.context; const { id: noteID } = note; const pixelHeight = elementHeights[noteID] || 10; const padding = 5; let noteHeight = scale(note.height); if (hasSpan(note)) { const upperHeight = scale(note.top_height); const harr = [ noteHeight - padding, upperHeight + padding ]; if (harr[0] - harr[1] > 0) { return new FlexibleNode(harr, pixelHeight); } noteHeight = (harr[0] + harr[1]) / 2; } return new Node(noteHeight, pixelHeight); } computeForceLayout(prevProps, prevState) { let { notes, nodes, elementHeights } = this.state; const { pixelHeight } = this.context; const { width, paddingLeft, forceOptions } = this.props; if (notes.length === 0) { return; } const v1 = Object.keys(nodes).length === notes.length; if (prevState == null) { prevState = {}; } const v2 = elementHeights === prevState.elementHeights || []; if (v1 && v2) { return; } const force = new Force({ minPos: 0, maxPos: pixelHeight, nodeSpacing: 0, ...forceOptions }); const dataNodes = notes.map(this.createNodeForNote); force.nodes(dataNodes).compute(); const _nodes = force.nodes() ?? []; const nodesObj = {}; for (let i = 0; i < _nodes.length; i++) { const node = _nodes[i]; const note = notes[i]; nodesObj[note.id] = node; } return this.updateState({ nodes: { $set: nodesObj } }); } updateHeight(id, height) { if (height == null) { return; } const { elementHeights } = this.state; elementHeights[id] = height; return this.updateState({ elementHeights: { $set: elementHeights } }); } updateNotes() { const { scaleClamped } = this.context; const notes = this.props.notes.filter(withinDomain(scaleClamped)).sort((a, b) => a.height - b.height); const columnIndex = notes.map(buildColumnIndex()); return this.setState({ notes, columnIndex }); } /* * Lifecycle methods */ componentDidMount() { this._previousContext = null; this.updateNotes(); return this.computeContextValue(); } componentDidUpdate(prevProps, prevState) { if (this.props.notes !== prevProps.notes) { this.updateNotes(); } const { noteComponent } = this.props; if (noteComponent !== prevProps.noteComponent) { this.setState({ noteComponent }); } this.computeForceLayout.call(prevProps, prevState); if (this.props.notes === prevProps.notes) { return; } if (this.context === this._previousContext) { return; } this.computeContextValue(); return this._previousContext = this.context; } } function NoteRect(props) { let { padding, width, ...rest } = props; if (padding == null) { padding = 5; } const { pixelHeight } = useContext(ColumnContext); if (width == null) { ({ width } = useContext(NoteLayoutContext)); } if (isNaN(width)) { return null; } return h("rect", { width: width + 2 * padding, height: pixelHeight, transform: `translate(${-padding},${-padding})`, ...rest }); } const NoteUnderlay = function({ ...rest }) { return h(NoteRect, { className: "underlay", ...rest }); }; function useNoteLayout() { const ctx = useContext(NoteLayoutContext); if (ctx == null) { throw new Error("useNoteLayout must be used within a NoteLayoutProvider"); } return ctx; } export { NoteLayoutContext, NoteLayoutProvider, NoteRect, NoteUnderlay, useNoteLayout }; //# sourceMappingURL=layout.js.map