@macrostrat/column-components
Version:
React rendering primitives for stratigraphic columns
257 lines (224 loc) • 5.59 kB
text/typescript
import {
Component,
createElement,
createContext,
useContext,
useRef,
ReactNode,
} from "react";
import h from "@macrostrat/hyper";
import { path } from "d3-path";
import {
ColumnLayoutContext,
ColumnLayoutCtx,
useColumnLayout,
} from "./context";
import { drawZigZagAtConstantHeight } from "./util";
let sequence = 0; // Initialize a sequence counter
function getUniqueIdentifier() {
// Generate a unique identifier using a sequential method that is stable across repeated
// re-renders. This evolves from a UUID-based approach for SSR (server-side rendering) compatibility.
const id = `uuid-${sequence}`;
sequence += 1; // Increment the sequence for the next call
return id;
}
class UUIDComponent<T> extends Component<T> {
UUID: string;
constructor(props: T) {
super(props);
this.UUID = getUniqueIdentifier();
}
}
const UUIDContext = createContext<string | null>(null);
function useBasicUUID() {
const ref = useRef<string>(getUniqueIdentifier());
return ref.current;
}
const useUUID = function () {
const uuid = useContext(UUIDContext);
if (uuid == null) {
return useBasicUUID();
}
return uuid;
};
function UUIDProvider({ children }) {
const ref = useRef<string>(getUniqueIdentifier());
return h(UUIDContext.Provider, { value: ref.current, children });
}
interface FrameProps {
id: string;
className?: string;
}
function SimpleFrame(props: FrameProps) {
const { pixelHeight: height, width } = useContext(ColumnLayoutContext);
let { id: frameID, className } = props;
if (frameID.startsWith("#")) {
frameID = frameID.slice(1);
}
return h("rect", {
id: frameID,
x: 0,
y: 0,
width,
height,
className,
});
}
interface GrainsizeFrameProps {
id: string;
zigZagBottom?: boolean;
zigZagTop?: boolean;
}
function GrainsizeFrame(props: GrainsizeFrameProps) {
let { id: frameID, zigZagBottom = false, zigZagTop = false } = props;
const { scale, divisions, grainsizeScale: gs } = useColumnLayout();
if (frameID.startsWith("#")) {
frameID = frameID.slice(1);
}
if (divisions.length === 0) {
return null;
}
const [bottomOfSection, topOfSection] = scale.domain();
const topOf = function (d) {
let { top } = d;
if (top > topOfSection) {
top = topOfSection;
}
return scale(top);
};
const bottomOf = function (d) {
let { bottom } = d;
if (bottom < bottomOfSection) {
bottom = bottomOfSection;
}
return scale(bottom);
};
const filteredDivisions = Array.from(divisions).filter(function (d) {
if (d.top <= bottomOfSection) {
return false;
}
if (d.bottom > topOfSection) {
return false;
}
return true;
});
let d = path();
let currentGrainsize = "m";
let i = 0;
for (const div of filteredDivisions) {
if (i === 0) {
// First division
// start the path at the bottom
const y = bottomOf(div);
d.moveTo(0, y);
}
if (div.grainsize != null) {
currentGrainsize = div.grainsize;
}
const x1 = gs(currentGrainsize);
if (i === 0 && zigZagBottom) {
// Draw zig-zag at bottom
drawZigZagAtConstantHeight(d, 0, x1, bottomOf(div));
} else {
// Draw a normal line
d.lineTo(x1, bottomOf(div));
}
d.lineTo(x1, bottomOf(div));
d.lineTo(x1, topOf(div));
if (i === filteredDivisions.length - 1) {
// Last division
// Draw top
if (zigZagTop) {
drawZigZagAtConstantHeight(d, x1, 0, topOf(div));
} else {
d.lineTo(0, topOf(div));
}
}
i++;
}
d.closePath();
return h("path", {
id: frameID,
key: frameID,
d: d.toString(),
});
}
const ClipPath = function (props) {
let { id, children, ...rest } = props;
if (id.startsWith("#")) {
id = id.slice(1);
}
return createElement("clipPath", { id, key: id, ...rest }, children);
};
const UseFrame = function (props) {
const { id: frameID, ...rest } = props;
return h("use.frame", {
xlinkHref: frameID,
fill: "transparent",
key: "frame",
...rest,
});
};
const prefixID = function (
uuid: string,
prefixes: string[],
): Record<string, string> {
const res = {};
for (let prefix of Array.from(prefixes)) {
res[prefix + "ID"] = `#${uuid}-${prefix}`;
}
return res;
};
export interface ClipToFrameProps {
left: number;
shiftY: number;
onClick?: () => void;
frame?: any;
width?: number;
className?: string;
children?: ReactNode;
clip?: boolean;
}
export function ClippingFrame(props: ClipToFrameProps) {
const {
left = 0,
shiftY = 0,
className,
onClick,
children,
frame = SimpleFrame,
clip = true,
} = props;
const uuid = useUUID();
const { frameID, clipID } = prefixID(uuid, ["frame", "clip"]);
let transform = null;
if (left != null) {
transform = `translate(${left} ${shiftY})`;
}
const frameClassName = "clip-frame column-clip-frame";
let _frame: ReactNode = h(frame, { id: frameID, className: frameClassName });
let defs = null;
let clipPath = null;
if (clip) {
defs = h("defs", { key: "defs" }, [
_frame,
h(ClipPath, { id: clipID }, h(UseFrame, { id: frameID })),
]);
clipPath = `url(${clipID})`;
_frame = h(UseFrame, { id: frameID, className: frameClassName });
}
return h("g", { className, transform, onClick }, [
defs,
h("g.inner", { clipPath }, children),
// Frame must go last
_frame,
]);
}
export {
SimpleFrame,
GrainsizeFrame,
ClipPath,
UUIDComponent,
UUIDProvider,
useUUID,
};