@macrostrat/column-components
Version:
React rendering primitives for stratigraphic columns
259 lines (258 loc) • 7.64 kB
JavaScript
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