@macrostrat/column-components
Version:
React rendering primitives for stratigraphic columns
1 lines • 15 kB
Source Map (JSON)
{"version":3,"file":"layout.cjs","sources":["../../src/notes/layout.ts"],"sourcesContent":["import { createContext, ReactNode, useContext } from \"react\";\nimport { StatefulComponent } from \"@macrostrat/ui-components\";\nimport h from \"../hyper\";\n\nimport { hasSpan } from \"./utils\";\nimport { FlexibleNode, Force, Node, Renderer } from \"./label-primitives\";\nimport {\n ColumnContext,\n ColumnCtx,\n ColumnDivision,\n ColumnLayoutProvider,\n} from \"../context\";\nimport {\n AgeRangeRelationship,\n compareAgeRanges,\n} from \"@macrostrat/stratigraphy-utils\";\n\nconst NoteLayoutContext = createContext(null);\n\nconst buildColumnIndex = function () {\n /*\n * Find out where on the X axis arrows,\n * etc. should plot to aviod overlaps\n */\n const heightTracker = [];\n return function (note) {\n let colIx = 0;\n // Get column that note should render in\n const nPossibleCols = heightTracker.length + 1;\n for (\n let column = 0, end = nPossibleCols, asc = 0 <= end;\n asc ? column <= end : column >= end;\n asc ? column++ : column--\n ) {\n if (heightTracker[column] == null) {\n heightTracker[column] = note.height;\n }\n if (heightTracker[column] < note.height) {\n const hy = note.top_height || note.height;\n heightTracker[column] = hy;\n colIx = column;\n break;\n }\n }\n return colIx;\n };\n};\n\nfunction withinDomain(scale) {\n const scaleDomain = scale.domain();\n const d1: [number, number] = [\n Math.min(...scaleDomain),\n Math.max(...scaleDomain),\n ];\n return (d) => {\n const noteRange: [number, number] = [d.height, d.top_height ?? d.height];\n const rel = compareAgeRanges(d1, noteRange);\n\n return rel !== AgeRangeRelationship.Disjoint;\n };\n}\n\ninterface NoteLayoutProviderProps {\n notes: any[];\n width: number;\n paddingLeft: number;\n noteComponent: any;\n forceOptions: object;\n children?: ReactNode;\n}\n\ninterface NoteLayoutState {\n notes?: any[];\n elementHeights?: object;\n columnIndex?: object;\n nodes?: object;\n updateHeight?: Function;\n generatePath: Function;\n createNodeForNote?: Function;\n noteComponent?: any;\n renderer?: typeof Renderer;\n}\n\nexport interface NoteLayoutCtx {\n renderer: typeof Renderer;\n paddingLeft: number;\n scale: Function;\n width: number;\n updateHeight: Function;\n generatePath: Function;\n columnIndex?: any;\n nodes?: any;\n}\n\nclass NoteLayoutProvider extends StatefulComponent<\n NoteLayoutProviderProps,\n NoteLayoutState\n> {\n static contextType = ColumnContext;\n static defaultProps = {\n paddingLeft: 60,\n estimatedTextHeight(note, width) {\n const txt = note.note || \"\";\n return 12;\n },\n };\n declare context: ColumnCtx<ColumnDivision>;\n _previousContext: ColumnCtx<ColumnDivision>;\n _rendererIndex: object;\n\n constructor(props) {\n super(props);\n this.computeContextValue = this.computeContextValue.bind(this);\n this.savedRendererForWidth = this.savedRendererForWidth.bind(this);\n this.generatePath = this.generatePath.bind(this);\n this.createNodeForNote = this.createNodeForNote.bind(this);\n this.computeForceLayout = this.computeForceLayout.bind(this);\n this.updateHeight = this.updateHeight.bind(this);\n this.updateNotes = this.updateNotes.bind(this);\n this.componentDidMount = this.componentDidMount.bind(this);\n this.componentDidUpdate = this.componentDidUpdate.bind(this);\n // State is very minimal to start\n const { noteComponent } = this.props;\n this.state = {\n notes: [],\n elementHeights: {},\n columnIndex: {},\n nodes: {},\n generatePath: this.generatePath,\n createNodeForNote: this.createNodeForNote,\n noteComponent,\n };\n }\n\n render() {\n const { children, width } = this.props;\n return h(\n NoteLayoutContext.Provider,\n { value: this.state },\n h(ColumnLayoutProvider, { width }, children),\n );\n }\n\n computeContextValue() {\n const { width, paddingLeft } = this.props;\n // Clamp notes to within scale boundaries\n // (we could turn this off if desired)\n const { scaleClamped: scale } = this.context;\n\n const forwardedValues = {\n // Forwarded values from column context\n // There may be a more elegant way to do this\n paddingLeft,\n scale,\n width,\n };\n\n // Compute force layout\n const renderer = new Renderer({\n direction: \"right\",\n layerGap: paddingLeft,\n nodeHeight: 5,\n });\n\n return this.setState({\n renderer,\n updateHeight: this.updateHeight,\n generatePath: this.generatePath,\n ...forwardedValues,\n });\n }\n\n savedRendererForWidth(width) {\n if (this._rendererIndex == null) {\n this._rendererIndex = {};\n }\n if (this._rendererIndex[width] == null) {\n this._rendererIndex[width] = new Renderer({\n direction: \"right\",\n layerGap: width,\n nodeHeight: 5,\n });\n }\n return this._rendererIndex[width];\n }\n\n generatePath(node, pixelOffset) {\n const { paddingLeft } = this.props;\n const renderer = this.savedRendererForWidth(paddingLeft - pixelOffset);\n try {\n return renderer.generatePath(node);\n } catch (err) {\n return null;\n }\n }\n\n createNodeForNote(note) {\n const { notes, elementHeights } = this.state;\n let { scaleClamped: scale } = this.context;\n const { id: noteID } = note;\n const pixelHeight = elementHeights[noteID] || 10;\n const padding = 5;\n let noteHeight = scale(note.height);\n if (hasSpan(note)) {\n const upperHeight = scale(note.top_height);\n const harr: [number, number] = [\n noteHeight - padding,\n upperHeight + padding,\n ];\n if (harr[0] - harr[1] > 0) {\n return new FlexibleNode(harr, pixelHeight);\n }\n noteHeight = (harr[0] + harr[1]) / 2;\n }\n return new Node(noteHeight, pixelHeight);\n }\n\n computeForceLayout(prevProps, prevState) {\n let { notes, nodes, elementHeights } = this.state;\n const { pixelHeight } = this.context;\n const { width, paddingLeft, forceOptions } = this.props;\n\n if (notes.length === 0) {\n return;\n }\n // Something is wrong...\n //return if elementHeights.length < notes.length\n // Return if we've already computed nodes\n const v1 = Object.keys(nodes).length === notes.length;\n if (prevState == null) {\n prevState = {};\n }\n const v2 = elementHeights === prevState.elementHeights || [];\n if (v1 && v2) {\n return;\n }\n\n const force = new Force({\n minPos: 0,\n maxPos: pixelHeight,\n nodeSpacing: 0,\n ...forceOptions,\n });\n\n const dataNodes = notes.map(this.createNodeForNote);\n\n force.nodes(dataNodes).compute();\n const _nodes = force.nodes() ?? [];\n const nodesObj = {};\n for (let i = 0; i < _nodes.length; i++) {\n const node = _nodes[i];\n const note = notes[i];\n nodesObj[note.id] = node;\n }\n\n return this.updateState({ nodes: { $set: nodesObj } });\n }\n\n updateHeight(id, height) {\n if (height == null) {\n return;\n }\n const { elementHeights } = this.state;\n elementHeights[id] = height;\n return this.updateState({ elementHeights: { $set: elementHeights } });\n }\n\n updateNotes() {\n // We received a new set of notes from props\n const { scaleClamped } = this.context;\n const notes = this.props.notes\n .filter(withinDomain(scaleClamped))\n .sort((a, b) => a.height - b.height);\n const columnIndex = notes.map(buildColumnIndex());\n return this.setState({ notes, columnIndex });\n }\n\n /*\n * Lifecycle methods\n */\n componentDidMount() {\n this._previousContext = null;\n this.updateNotes();\n return this.computeContextValue();\n }\n\n componentDidUpdate(prevProps, prevState) {\n if (this.props.notes !== prevProps.notes) {\n this.updateNotes();\n }\n\n // Update note component\n const { noteComponent } = this.props;\n if (noteComponent !== prevProps.noteComponent) {\n this.setState({ noteComponent });\n }\n this.computeForceLayout.call(prevProps, prevState);\n if (this.props.notes === prevProps.notes) {\n return;\n }\n if (this.context === this._previousContext) {\n return;\n }\n this.computeContextValue();\n return (this._previousContext = this.context);\n }\n}\n\nfunction NoteRect(props) {\n let { padding, width, ...rest } = props;\n if (padding == null) {\n padding = 5;\n }\n const { pixelHeight } = useContext(ColumnContext);\n if (width == null) {\n ({ width } = useContext(NoteLayoutContext));\n }\n if (isNaN(width)) {\n return null;\n }\n\n return h(\"rect\", {\n width: width + 2 * padding,\n height: pixelHeight,\n transform: `translate(${-padding},${-padding})`,\n ...rest,\n });\n}\n\nconst NoteUnderlay = function ({ ...rest }) {\n return h(NoteRect, {\n className: \"underlay\",\n ...rest,\n });\n};\n\nexport function useNoteLayout() {\n const ctx = useContext(NoteLayoutContext);\n if (ctx == null) {\n throw new Error(\"useNoteLayout must be used within a NoteLayoutProvider\");\n }\n return ctx;\n}\n\nexport { NoteLayoutContext, NoteLayoutProvider, NoteRect, NoteUnderlay };\n"],"names":["createContext","column","compareAgeRanges","AgeRangeRelationship","StatefulComponent","ColumnContext","h","ColumnLayoutProvider","Renderer","hasSpan","FlexibleNode","Node","Force","useContext"],"mappings":"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"}