@macrostrat/column-views 3.12.1 → 3.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"composite-scale.js","sources":["../../src/prepare-units/composite-scale.ts"],"sourcesContent":["import { ColumnAxisType } from \"@macrostrat/column-components\";\nimport { agesOverlap, ensureArray, getUnitHeightRange } from \"./utils\";\nimport { ScaleContinuousNumeric, scaleLinear } from \"d3-scale\";\nimport { UnitLong } from \"@macrostrat/api-types\";\nimport { buildHybridScale } from \"./dynamic-scales\";\nimport { sectionDensity } from \"./density\";\nimport { ExtUnit, HybridScaleType, SectionInfo } from \"./types\";\nimport type {\n ColumnScaleOptions,\n CompositeColumnData,\n CompositeScaleData,\n PackageLayoutData,\n PackageScaleInfo,\n PackageScaleLayoutData,\n SectionScaleOptions,\n} from \"./types\";\n\n// Composite scale information augmented with units in each package\n\nexport function buildCompositeScaleInfo(\n inputScales: PackageScaleInfo[],\n unconformityHeight: number,\n): CompositeScaleData {\n /** Finalize the heights of sections, including the heights of unconformities\n * between them.\n */\n\n let totalHeight = unconformityHeight / 2;\n let lastSectionTopHeight = 0;\n\n const packages2: PackageScaleLayoutData[] = [];\n for (let i = 0; i < inputScales.length; i++) {\n const group = inputScales[i];\n const { domain, scale } = group;\n const [b_age, t_age] = domain;\n // Positionally stable key: packages are always emitted in age order, so the\n // index is a stable identity. An age-based key (`package-${b_age}-${t_age}`)\n // changes every frame while the age window animates, forcing every package,\n // column, and timescale slice in the correlation chart to remount each frame.\n const key = `package-${i}`;\n\n packages2.push({\n ...createPackageScale(group, totalHeight),\n key,\n offset: totalHeight,\n // Unconformity height above this particular section\n paddingTop: totalHeight - lastSectionTopHeight,\n });\n\n const pixelHeight = Math.abs(scale(b_age) - scale(t_age));\n lastSectionTopHeight = totalHeight + pixelHeight;\n totalHeight = lastSectionTopHeight + unconformityHeight;\n }\n totalHeight += unconformityHeight / 2;\n return {\n totalHeight,\n sections: packages2,\n };\n}\n\nexport function finalizeSectionHeights<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n unconformityHeight: number,\n): CompositeColumnData<T> {\n /** Finalize the heights of sections, including the heights of unconformities\n * between them.\n */\n\n const sectionScales = sections.map((d) => d.scaleInfo);\n\n const { totalHeight, sections: packages } = buildCompositeScaleInfo(\n sectionScales,\n unconformityHeight,\n );\n\n // This could perhaps be simplified.\n const sections1: PackageLayoutData<T>[] = [];\n for (const i in sections) {\n const group = sections[i];\n const scaleInfo = packages[i];\n sections1.push({\n ...group,\n key: scaleInfo.key,\n scaleInfo,\n });\n }\n return {\n totalHeight,\n sections: sections1,\n };\n}\n\nexport interface SectionInfoWithScale<\n T extends UnitLong = ExtUnit,\n> extends SectionInfo<T> {\n scaleInfo: PackageScaleInfo;\n}\n\nexport function computeSectionHeights<T extends UnitLong>(\n sections: SectionInfo<T>[],\n opts: ColumnScaleOptions,\n): SectionInfoWithScale<T>[] {\n return sections.map((group) => {\n return addScaleToSection<T>(group, opts);\n });\n}\n\nfunction addScaleToSection<T extends UnitLong = ExtUnit>(\n group: SectionInfo<T>,\n opts: ColumnScaleOptions,\n): SectionInfoWithScale<T> {\n const { t_age, b_age, t_pos, b_pos, units } = group;\n let _range = null;\n // if t_age and b_age are set for a group, use them to define the range...\n if (opts.axisType == ColumnAxisType.AGE) {\n _range = [b_age, t_age];\n } else {\n _range = [b_pos, t_pos];\n }\n\n const scaleInfo = buildSectionScale<T>(units, {\n ...opts,\n domain: _range,\n sectionID: group.section_id,\n });\n\n return {\n ...group,\n scaleInfo,\n };\n}\n\nfunction buildSectionScale<T extends UnitLong>(\n data: T[],\n opts: SectionScaleOptions,\n): PackageScaleInfo {\n const {\n targetUnitHeight = 20,\n axisType,\n visibleWindow,\n scale,\n hybridScale,\n sectionID,\n } = opts;\n const domain = opts.domain ?? findSectionHeightRange(data, axisType);\n\n const dAge = Math.abs(domain[0] - domain[1]);\n\n let _pixelScale: any = opts.pixelScale;\n let pixelHeight: number;\n\n if (hybridScale != null) {\n /** In an equidistant surfaces scale, we want to determine the heights of surfaces\n * and then distribute units evenly between them.\n * This is somewhat like an ordinal scale\n */\n if (hybridScale.type === HybridScaleType.EquidistantSurfaces) {\n // Pixels between one surface and the next: neither axis's units\n _pixelScale ??= targetUnitHeight;\n } else {\n // An approximate-height column is drawn from measured thickness, so it\n // is metres per pixel whatever the axis is labeled in\n _pixelScale = opts.pixelsPerMeter ?? _pixelScale;\n }\n\n return buildHybridScale(hybridScale, data, domain, {\n pixelOffset: 0,\n pixelScale: _pixelScale,\n });\n }\n\n if (scale == null) {\n /** Every rule that sets a section's height resolves to one density (see\n * `./density`). Sections reach here at their *full* extent — the rendered\n * window is applied afterwards, by `trimSectionsToWindow` — so a floor on\n * the section's height only ever inflates a genuinely small section, which\n * is what it's for, and never a sliver the window happens to cut. */\n _pixelScale = sectionDensity(opts)({\n extent: dAge,\n unitExtents: visibleUnitExtents(data, axisType, visibleWindow),\n units: data,\n sectionID,\n axisType,\n });\n pixelHeight = dAge * _pixelScale;\n } else {\n // If a scale is provided, use it to compute pixel height\n pixelHeight = Math.abs(scale(domain[0]) - scale(domain[1]));\n }\n\n return createPackageScale(\n { scale, domain, pixelHeight, pixelScale: _pixelScale },\n 0,\n );\n}\n\n/**\n * The density each section will actually render at once the window is applied.\n *\n * A section the window cuts short is still being *looked at*, so\n * `minSectionHeight` applies to what remains — it's honored by expanding the\n * section's scale, not by showing more of it. Resolving this **before** padding\n * matters: padding is a pixel margin measured at the density it will render at,\n * and a section stretched to the floor afterwards would multiply the margin\n * along with everything else (a 20 px margin in a band stretched 4× renders at\n * 80 px, and asymmetrically so whenever one side is truncated).\n *\n * Sections the window leaves whole keep their laid-out density — they already\n * met the floor when they were sized.\n */\nexport function resolveWindowScales<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n focalWindow: [number, number],\n floor: number,\n): Map<SectionInfoWithScale<T>, number> {\n const [b_focal, t_focal] = focalWindow;\n const scales = new Map<SectionInfoWithScale<T>, number>();\n\n for (const section of sections) {\n const t_age = Math.max(section.t_age, t_focal);\n const b_age = Math.min(section.b_age, b_focal);\n // Sections outside the focal window are visible only as padding; they keep\n // their own density and render exactly the pixels asked for.\n if (b_age <= t_age) continue;\n\n const natural = section.scaleInfo.pixelScale;\n if (!(natural > 0)) continue;\n\n const isClipped = b_age < section.b_age || t_age > section.t_age;\n const needed = isClipped && floor > 0 ? floor / (b_age - t_age) : 0;\n scales.set(section, Math.max(natural, needed));\n }\n return scales;\n}\n\n/**\n * Expand an age window outward by `padding` **pixels** of column, spending the\n * budget against sections that are already laid out at full extent, at the\n * densities they will finally render at (`scaleFor`).\n *\n * Because the layout is done, this is exact rather than predicted: each section\n * contributes its own real pixel height. Padding collapses when the column runs\n * out on that side.\n *\n * Unconformities between sections cost nothing. They render at a fixed height\n * that's typically larger than the padding itself (30–60 px), so charging them\n * would mean padding could never reach across a gap — which is the case it\n * exists for: revealing the neighboring section keeps its timescale intervals\n * navigable. The padding is a budget of neighboring *content*.\n */\nexport function padWindowByPixels<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n window: [number, number],\n padding: number,\n scaleFor: (section: SectionInfoWithScale<T>) => number,\n): [number, number] {\n if (!(padding > 0) || sections.length === 0) return window;\n const [b_age, t_age] = window;\n return [\n spendOutward(sections, b_age, 1, padding, scaleFor),\n spendOutward(sections, t_age, -1, padding, scaleFor),\n ];\n}\n\n/** Walk out from one edge of the window through laid-out sections, spending a\n * pixel budget, and return the age it lands on. `direction` is +1 toward older\n * ages, -1 toward younger. */\nfunction spendOutward<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n edge: number,\n direction: 1 | -1,\n padding: number,\n scaleFor: (section: SectionInfoWithScale<T>) => number,\n): number {\n // Sections ordered outward from the edge, keeping only what lies beyond it.\n const beyond = sections\n .filter(\n (s) => direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,\n )\n .sort((a, b) => direction * (nearEdge(a, direction) - nearEdge(b, direction)));\n\n let bound = edge;\n let budget = padding;\n\n for (const section of beyond) {\n const pixelScale = scaleFor(section);\n if (!(pixelScale > 0)) continue;\n\n const from =\n direction > 0\n ? Math.max(section.t_age, bound)\n : Math.min(section.b_age, bound);\n const available = direction > 0 ? section.b_age - from : from - section.t_age;\n if (available <= 0) continue;\n\n const cost = available * pixelScale;\n if (budget <= cost) return from + direction * (budget / pixelScale);\n\n bound = direction > 0 ? section.b_age : section.t_age;\n budget -= cost;\n }\n return bound;\n}\n\n/** The age at which a section starts, measured from the window edge outward. */\nfunction nearEdge<T extends UnitLong>(\n section: SectionInfoWithScale<T>,\n direction: 1 | -1,\n): number {\n return direction > 0 ? section.t_age : section.b_age;\n}\n\n/**\n * Clip laid-out sections to the rendered window — the last step of layout, so\n * that every section has already been sized (and floored) at its full extent.\n * Each section renders at the density `scaleFor` resolved for it, which is what\n * keeps a padding margin the same number of pixels as the band it borders is\n * stretched to meet `minSectionHeight`.\n */\nexport function trimSectionsToWindow<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n window: [number, number],\n scaleFor: (section: SectionInfoWithScale<T>) => number,\n): SectionInfoWithScale<T>[] {\n const [b_win, t_win] = window;\n const out: SectionInfoWithScale<T>[] = [];\n\n for (const section of sections) {\n const t_age = Math.max(section.t_age, t_win);\n const b_age = Math.min(section.b_age, b_win);\n if (b_age <= t_age) continue;\n\n const pixelScale = scaleFor(section);\n if (\n b_age === section.b_age &&\n t_age === section.t_age &&\n pixelScale === section.scaleInfo.pixelScale\n ) {\n out.push(section);\n continue;\n }\n\n out.push({\n ...section,\n t_age,\n b_age,\n units: section.units.filter((u) => agesOverlap(u, { t_age, b_age })),\n scaleInfo: createPackageScale({\n domain: [b_age, t_age],\n pixelScale,\n pixelHeight: (b_age - t_age) * pixelScale,\n scale: null,\n }),\n });\n }\n return out;\n}\n\nexport function createPackageScale(\n def: PackageScaleInfo,\n offset: number = 0,\n): PackageScaleInfo {\n /** Build a section scale */\n // Domain should be oriented from bottom to top, but scale is oriented from top to bottom\n const { domain, pixelScale, pixelHeight, scale, heightScale } = def;\n\n if (scale == null && pixelScale == null) {\n throw new Error(\"Either scale or pixelScale must be provided\");\n }\n\n let _scale: ScaleContinuousNumeric<number, number>;\n if (scale == null) {\n _scale = scaleLinear()\n .domain([domain[1], domain[0]])\n .range([offset, pixelHeight + offset]);\n } else {\n const domain0 = scale.domain();\n const range0 = scale.range();\n _scale = scale\n .copy()\n .domain(domain0)\n .range(range0.map((d) => d + offset));\n }\n\n _scale.clamp();\n\n let _heightScale = null;\n if (heightScale != null) {\n // Adjust height scale as well\n const range0 = scale.range();\n _heightScale = heightScale.copy().range(range0.map((d) => d + offset));\n _heightScale.clamp();\n }\n\n return {\n domain,\n pixelScale,\n pixelHeight,\n scale: _scale,\n // Internal details for hybrid scales. TODO: improve this\n heightScale: _heightScale,\n };\n}\n\nfunction findSectionHeightRange(\n data: UnitLong[],\n axisType: ColumnAxisType,\n): [number, number] {\n if (axisType == null) {\n throw new Error(\"Axis type is not set\");\n }\n if (axisType === ColumnAxisType.AGE) {\n const t_age = Math.min(...data.map((d) => d.t_age));\n const b_age = Math.max(...data.map((d) => d.b_age));\n return [b_age, t_age];\n } else if (\n axisType == ColumnAxisType.DEPTH ||\n axisType == ColumnAxisType.ORDINAL\n ) {\n const t_pos = Math.min(...data.map((d) => d.t_pos));\n const b_pos = Math.max(...data.map((d) => d.b_pos));\n return [b_pos, t_pos];\n } else if (axisType == ColumnAxisType.HEIGHT) {\n const t_pos = Math.max(...data.map((d) => d.t_pos));\n const b_pos = Math.min(...data.map((d) => d.b_pos));\n return [b_pos, t_pos];\n }\n}\n\nfunction visibleUnitExtents(\n data: UnitLong[],\n axisType: ColumnAxisType,\n visibleWindow?: [number, number] | null,\n): number[] {\n /** The durations of the units a density rule gets to size.\n *\n * Measured over what the render window actually *shows* — units outside it\n * are ignored and a unit it cuts through counts only for its visible part.\n * That's what makes `targetUnitHeight` mean the same thing at every zoom\n * depth: the units on screen are drawn at the target height whether they're\n * whole or slivers of something much longer.\n *\n * A section the window doesn't reach has nothing visible to measure, so it\n * falls back to its own units at full duration — it's drawn at its own scale.\n * (Deriving it from the padded window instead would be circular: padding is\n * measured in pixels, which is what we're trying to establish.)\n */\n const durations = (clip: boolean) =>\n data\n .map((d) => {\n const [b_pos, t_pos] = getUnitHeightRange(d, axisType);\n if (!clip) return Math.abs(b_pos - t_pos);\n const [b_win, t_win] = visibleWindow;\n return Math.max(\n 0,\n Math.min(b_pos, b_win) - Math.max(t_pos, t_win),\n );\n })\n .filter((d) => d > 0);\n\n const useWindow = visibleWindow != null && axisType === ColumnAxisType.AGE;\n let heights = useWindow ? durations(true) : [];\n if (heights.length === 0) heights = durations(false);\n return heights;\n}\n\nexport interface CompositeColumnScale {\n (age: number): number | null;\n copy(): CompositeColumnScale;\n domain(): [number, number];\n invert(pixelHeight: number): number | null;\n clamp(clamp: boolean): void;\n}\n\nexport function createCompositeScale(\n sections: PackageLayoutData[],\n interpolateUnconformities: boolean = false,\n): CompositeColumnScale {\n /** Create a scale that works across multiple packages */\n\n const scales: ScaleContinuousNumeric<number, number>[] = [];\n\n let lastScale: ScaleContinuousNumeric<number, number> | null = null;\n for (const section of sections) {\n const _scale = section.scaleInfo.scale.copy().clamp(true);\n scales.push(_scale);\n if (lastScale != null && interpolateUnconformities) {\n // Add a new scale that interpolates between lastScale and _scale\n const lastDomain = lastScale.domain();\n const lastRange = lastScale.range();\n const currentDomain = _scale.domain();\n const currentRange = _scale.range();\n\n const interpScale = scaleLinear()\n .domain([lastDomain[lastDomain.length - 1], currentDomain[0]])\n .range([lastRange[lastRange.length - 1], currentRange[0]])\n .clamp(true);\n scales.push(interpScale);\n }\n lastScale = _scale;\n }\n\n const scale: CompositeColumnScale = (age) => {\n for (const s of scales) {\n const domain = s.domain();\n if (\n domain[0] < domain[domain.length - 1] &&\n age >= domain[0] &&\n age <= domain[domain.length - 1]\n ) {\n // Age axes\n return s(age);\n } else if (age <= domain[0] && age >= domain[domain.length - 1]) {\n // Normal axes like height\n return s(age);\n }\n }\n return null;\n };\n\n scale.copy = () => {\n return createCompositeScale(sections, interpolateUnconformities);\n };\n\n scale.domain = () => {\n /** Return the domain of the scale */\n const vals = sections.flatMap((d) => d.scaleInfo.domain);\n if (vals[0] < vals[vals.length - 1]) {\n // age axes\n return [Math.min(...vals), Math.max(...vals)];\n }\n // Normal axes like height\n return [Math.max(...vals), Math.min(...vals)];\n };\n\n scale.invert = (pixelHeight) => {\n /** Invert the scale to get the age at a given pixel height */\n // Iterate through the sections to find the correct one\n for (const scale of scales) {\n const range = scale.range();\n if (\n pixelHeight > Math.min(...range) &&\n pixelHeight <= Math.max(...range)\n ) {\n //console.log(\"Inverting scale at pixel height\", pixelHeight);\n return scale.invert(pixelHeight);\n }\n }\n return null;\n };\n\n scale.clamp = (clamp: boolean) => {\n /** Clamp all constituent scales */\n for (const s of scales) {\n s.clamp(clamp);\n }\n };\n\n return scale;\n}\n\n/** Collapse sections separated by unconformities that are smaller than a given pixel height. */\nexport function collapseUnconformitiesByPixelHeight<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n threshold: number,\n opts: ColumnScaleOptions,\n): SectionInfoWithScale<T>[] {\n const newSections = [];\n let currentSection: SectionInfoWithScale<T> | null = null;\n for (const nextSection of sections) {\n if (currentSection == null) {\n currentSection = nextSection;\n continue;\n }\n let heights: [number, number];\n let pxHeights: [number, number];\n if (opts.axisType !== ColumnAxisType.AGE) {\n heights = [nextSection.t_pos, currentSection.b_pos];\n } else {\n heights = [nextSection.t_age, currentSection.b_age];\n }\n\n const _diff = (vals: number[]) => {\n return Math.abs(vals[0] - vals[1]);\n };\n\n pxHeights = [\n _diff(heights.map(nextSection.scaleInfo.scale)),\n _diff(heights.map(currentSection.scaleInfo.scale)),\n ];\n\n /** The gap has no density of its own — it falls between two sections that\n * may be drawn at very different ones — so it is judged at the finer of\n * the two. Taking the smaller estimate let a sparse neighbor speak for a\n * gap the other neighbor would have drawn many times larger: a 16 Myr\n * hiatus in column 22 read as 26px against one section and 166px against\n * the other, and collapsed. A gap is only worth hiding when neither scale\n * would give it more room than the break that replaces it. */\n const pxHeight = Math.max(...pxHeights);\n\n if (pxHeight < threshold) {\n let t_pos: number;\n let b_pos: number;\n if (\n opts.axisType == ColumnAxisType.AGE ||\n opts.axisType == ColumnAxisType.DEPTH\n ) {\n t_pos = Math.min(currentSection.t_pos, nextSection.t_pos);\n b_pos = Math.max(currentSection.b_pos, nextSection.b_pos);\n } else {\n t_pos = Math.max(currentSection.t_pos, nextSection.t_pos);\n b_pos = Math.min(currentSection.b_pos, nextSection.b_pos);\n }\n\n // We need to merge the sections\n const compositeSection0: SectionInfo<T> = {\n units: [...currentSection.units, ...nextSection.units],\n section_id: [\n ...ensureArray(currentSection.section_id),\n ...ensureArray(nextSection.section_id),\n ],\n t_age: Math.min(currentSection.t_age, nextSection.t_age),\n b_age: Math.max(currentSection.b_age, nextSection.b_age),\n t_pos,\n b_pos,\n };\n\n currentSection = addScaleToSection(compositeSection0, opts);\n } else {\n // We need to keep the section\n newSections.push(currentSection);\n currentSection = nextSection;\n }\n }\n if (currentSection != null) {\n newSections.push(currentSection);\n }\n\n return newSections;\n}\n"],"names":["scale"],"mappings":"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"}
1
+ {"version":3,"file":"composite-scale.js","sources":["../../src/prepare-units/composite-scale.ts"],"sourcesContent":["import { ColumnAxisType } from \"@macrostrat/column-components\";\nimport { agesOverlap, ensureArray, getUnitHeightRange } from \"./utils\";\nimport { ScaleContinuousNumeric, scaleLinear } from \"d3-scale\";\nimport { UnitLong } from \"@macrostrat/api-types\";\nimport { buildHybridScale } from \"./dynamic-scales\";\nimport { sectionDensity } from \"./density\";\nimport type {\n ColumnScaleOptions,\n CompositeColumnData,\n CompositeScaleData,\n PackageLayoutData,\n PackageScaleInfo,\n PackageScaleLayoutData,\n SectionScaleOptions,\n} from \"./types\";\nimport { ExtUnit, HybridScaleType, SectionInfo } from \"./types\";\nimport { mergeAgeRanges, MergeMode } from \"@macrostrat/stratigraphy-utils\";\n\n// Composite scale information augmented with units in each package\n\nexport function buildCompositeScaleInfo(\n inputScales: PackageScaleInfo[],\n unconformityHeight: number,\n): CompositeScaleData {\n /** Finalize the heights of sections, including the heights of unconformities\n * between them.\n */\n\n let totalHeight = unconformityHeight / 2;\n let lastSectionTopHeight = 0;\n\n const packages2: PackageScaleLayoutData[] = [];\n for (let i = 0; i < inputScales.length; i++) {\n const group = inputScales[i];\n const { domain, scale } = group;\n const [b_age, t_age] = domain;\n // Positionally stable key: packages are always emitted in age order, so the\n // index is a stable identity. An age-based key (`package-${b_age}-${t_age}`)\n // changes every frame while the age window animates, forcing every package,\n // column, and timescale slice in the correlation chart to remount each frame.\n const key = `package-${i}`;\n\n packages2.push({\n ...createPackageScale(group, totalHeight),\n key,\n offset: totalHeight,\n // Unconformity height above this particular section\n paddingTop: totalHeight - lastSectionTopHeight,\n });\n\n const pixelHeight = Math.abs(scale(b_age) - scale(t_age));\n lastSectionTopHeight = totalHeight + pixelHeight;\n totalHeight = lastSectionTopHeight + unconformityHeight;\n }\n totalHeight += unconformityHeight / 2;\n return {\n totalHeight,\n sections: packages2,\n };\n}\n\nexport function finalizeSectionHeights<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n unconformityHeight: number,\n): CompositeColumnData<T> {\n /** Finalize the heights of sections, including the heights of unconformities\n * between them.\n */\n\n const sectionScales = sections.map((d) => d.scaleInfo);\n\n const { totalHeight, sections: packages } = buildCompositeScaleInfo(\n sectionScales,\n unconformityHeight,\n );\n\n // This could perhaps be simplified.\n const sections1: PackageLayoutData<T>[] = [];\n for (const i in sections) {\n const group = sections[i];\n const scaleInfo = packages[i];\n sections1.push({\n ...group,\n key: scaleInfo.key,\n scaleInfo,\n });\n }\n return {\n totalHeight,\n sections: sections1,\n };\n}\n\nexport interface SectionInfoWithScale<\n T extends UnitLong = ExtUnit,\n> extends SectionInfo<T> {\n scaleInfo: PackageScaleInfo;\n}\n\nexport function computeSectionHeights<T extends UnitLong>(\n sections: SectionInfo<T>[],\n opts: ColumnScaleOptions,\n): SectionInfoWithScale<T>[] {\n return sections.map((group) => {\n return addScaleToSection<T>(group, opts);\n });\n}\n\nfunction addScaleToSection<T extends UnitLong = ExtUnit>(\n group: SectionInfo<T>,\n opts: ColumnScaleOptions,\n): SectionInfoWithScale<T> {\n const { t_age, b_age, t_pos, b_pos, units } = group;\n let _range = null;\n // if t_age and b_age are set for a group, use them to define the range...\n if (opts.axisType == ColumnAxisType.AGE) {\n _range = [b_age, t_age];\n } else {\n _range = [b_pos, t_pos];\n }\n\n const scaleInfo = buildSectionScale<T>(units, {\n ...opts,\n domain: _range,\n sectionID: group.section_id,\n });\n\n return {\n ...group,\n scaleInfo,\n };\n}\n\nfunction buildSectionScale<T extends UnitLong>(\n data: T[],\n opts: SectionScaleOptions,\n): PackageScaleInfo {\n const {\n targetUnitHeight = 20,\n axisType,\n visibleWindow,\n scale,\n hybridScale,\n sectionID,\n } = opts;\n const domain = opts.domain ?? findSectionHeightRange(data, axisType);\n\n const dAge = dx(domain);\n let visibleExtent = dAge; // full section is visible\n if (visibleWindow != null) {\n visibleExtent = dx(\n mergeAgeRanges([domain, visibleWindow], MergeMode.Inner),\n );\n }\n\n let _pixelScale: any = opts.pixelScale;\n let pixelHeight: number;\n\n if (hybridScale != null) {\n /** In an equidistant surfaces scale, we want to determine the heights of surfaces\n * and then distribute units evenly between them.\n * This is somewhat like an ordinal scale\n */\n if (hybridScale.type === HybridScaleType.EquidistantSurfaces) {\n // Pixels between one surface and the next: neither axis's units\n _pixelScale ??= targetUnitHeight;\n } else {\n // An approximate-height column is drawn from measured thickness, so it\n // is metres per pixel whatever the axis is labeled in\n _pixelScale = opts.pixelsPerMeter ?? _pixelScale;\n }\n\n return buildHybridScale(hybridScale, data, domain, {\n pixelOffset: 0,\n pixelScale: _pixelScale,\n });\n }\n\n if (scale == null) {\n /** Every rule that sets a section's height resolves to one density (see\n * `./density`). Sections reach here at their *full* extent — the rendered\n * window is applied afterwards, by `trimSectionsToWindow` — so a floor on\n * the section's height only ever inflates a genuinely small section, which\n * is what it's for, and never a sliver the window happens to cut. */\n _pixelScale = sectionDensity(opts)({\n extent: dAge,\n visibleExtent,\n unitExtents: visibleUnitExtents(data, axisType, visibleWindow),\n units: data,\n sectionID,\n axisType,\n });\n pixelHeight = dAge * _pixelScale;\n } else {\n // If a scale is provided, use it to compute pixel height\n pixelHeight = Math.abs(scale(domain[0]) - scale(domain[1]));\n }\n\n return createPackageScale(\n { scale, domain, pixelHeight, pixelScale: _pixelScale },\n 0,\n );\n}\n\nfunction dx(range: [number, number]) {\n return Math.abs(range[0] - range[1]);\n}\n\n/**\n * The density each section will actually render at once the window is applied.\n *\n * A section the window cuts short is still being *looked at*, so\n * `minSectionHeight` applies to what remains — it's honored by expanding the\n * section's scale, not by showing more of it. Resolving this **before** padding\n * matters: padding is a pixel margin measured at the density it will render at,\n * and a section stretched to the floor afterwards would multiply the margin\n * along with everything else (a 20 px margin in a band stretched 4× renders at\n * 80 px, and asymmetrically so whenever one side is truncated).\n *\n * Sections the window leaves whole keep their laid-out density — they already\n * met the floor when they were sized.\n */\nexport function resolveWindowScales<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n focalWindow: [number, number],\n floor: number,\n): Map<SectionInfoWithScale<T>, number> {\n const [b_focal, t_focal] = focalWindow;\n const scales = new Map<SectionInfoWithScale<T>, number>();\n\n for (const section of sections) {\n const t_age = Math.max(section.t_age, t_focal);\n const b_age = Math.min(section.b_age, b_focal);\n // Sections outside the focal window are visible only as padding; they keep\n // their own density and render exactly the pixels asked for.\n if (b_age <= t_age) continue;\n\n const natural = section.scaleInfo.pixelScale;\n if (!(natural > 0)) continue;\n\n const isClipped = b_age < section.b_age || t_age > section.t_age;\n const needed = isClipped && floor > 0 ? floor / (b_age - t_age) : 0;\n scales.set(section, Math.max(natural, needed));\n }\n return scales;\n}\n\n/**\n * Expand an age window outward by `padding` **pixels** of column, spending the\n * budget against sections that are already laid out at full extent, at the\n * densities they will finally render at (`scaleFor`).\n *\n * Because the layout is done, this is exact rather than predicted: each section\n * contributes its own real pixel height. Padding collapses when the column runs\n * out on that side.\n *\n * Unconformities between sections cost nothing. They render at a fixed height\n * that's typically larger than the padding itself (30–60 px), so charging them\n * would mean padding could never reach across a gap — which is the case it\n * exists for: revealing the neighboring section keeps its timescale intervals\n * navigable. The padding is a budget of neighboring *content*.\n */\nexport function padWindowByPixels<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n window: [number, number],\n padding: number,\n scaleFor: (section: SectionInfoWithScale<T>) => number,\n): [number, number] {\n if (!(padding > 0) || sections.length === 0) return window;\n const [b_age, t_age] = window;\n return [\n spendOutward(sections, b_age, 1, padding, scaleFor),\n spendOutward(sections, t_age, -1, padding, scaleFor),\n ];\n}\n\n/** Walk out from one edge of the window through laid-out sections, spending a\n * pixel budget, and return the age it lands on. `direction` is +1 toward older\n * ages, -1 toward younger. */\nfunction spendOutward<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n edge: number,\n direction: 1 | -1,\n padding: number,\n scaleFor: (section: SectionInfoWithScale<T>) => number,\n): number {\n // Sections ordered outward from the edge, keeping only what lies beyond it.\n const beyond = sections\n .filter(\n (s) =>\n direction * (s.b_age - edge) > 0 || direction * (s.t_age - edge) > 0,\n )\n .sort(\n (a, b) => direction * (nearEdge(a, direction) - nearEdge(b, direction)),\n );\n\n let bound = edge;\n let budget = padding;\n\n for (const section of beyond) {\n const pixelScale = scaleFor(section);\n if (!(pixelScale > 0)) continue;\n\n const from =\n direction > 0\n ? Math.max(section.t_age, bound)\n : Math.min(section.b_age, bound);\n const available =\n direction > 0 ? section.b_age - from : from - section.t_age;\n if (available <= 0) continue;\n\n const cost = available * pixelScale;\n if (budget <= cost) return from + direction * (budget / pixelScale);\n\n bound = direction > 0 ? section.b_age : section.t_age;\n budget -= cost;\n }\n return bound;\n}\n\n/** The age at which a section starts, measured from the window edge outward. */\nfunction nearEdge<T extends UnitLong>(\n section: SectionInfoWithScale<T>,\n direction: 1 | -1,\n): number {\n return direction > 0 ? section.t_age : section.b_age;\n}\n\n/**\n * Clip laid-out sections to the rendered window — the last step of layout, so\n * that every section has already been sized (and floored) at its full extent.\n * Each section renders at the density `scaleFor` resolved for it, which is what\n * keeps a padding margin the same number of pixels as the band it borders is\n * stretched to meet `minSectionHeight`.\n */\nexport function trimSectionsToWindow<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n window: [number, number],\n scaleFor: (section: SectionInfoWithScale<T>) => number,\n): SectionInfoWithScale<T>[] {\n const [b_win, t_win] = window;\n const out: SectionInfoWithScale<T>[] = [];\n\n for (const section of sections) {\n const t_age = Math.max(section.t_age, t_win);\n const b_age = Math.min(section.b_age, b_win);\n if (b_age <= t_age) continue;\n\n const pixelScale = scaleFor(section);\n if (\n b_age === section.b_age &&\n t_age === section.t_age &&\n pixelScale === section.scaleInfo.pixelScale\n ) {\n out.push(section);\n continue;\n }\n\n out.push({\n ...section,\n t_age,\n b_age,\n units: section.units.filter((u) => agesOverlap(u, { t_age, b_age })),\n scaleInfo: createPackageScale({\n domain: [b_age, t_age],\n pixelScale,\n pixelHeight: (b_age - t_age) * pixelScale,\n scale: null,\n }),\n });\n }\n return out;\n}\n\nexport function createPackageScale(\n def: PackageScaleInfo,\n offset: number = 0,\n): PackageScaleInfo {\n /** Build a section scale */\n // Domain should be oriented from bottom to top, but scale is oriented from top to bottom\n const { domain, pixelScale, pixelHeight, scale, heightScale } = def;\n\n if (scale == null && pixelScale == null) {\n throw new Error(\"Either scale or pixelScale must be provided\");\n }\n\n let _scale: ScaleContinuousNumeric<number, number>;\n if (scale == null) {\n _scale = scaleLinear()\n .domain([domain[1], domain[0]])\n .range([offset, pixelHeight + offset]);\n } else {\n const domain0 = scale.domain();\n const range0 = scale.range();\n _scale = scale\n .copy()\n .domain(domain0)\n .range(range0.map((d) => d + offset));\n }\n\n _scale.clamp();\n\n let _heightScale = null;\n if (heightScale != null) {\n // Adjust height scale as well\n const range0 = scale.range();\n _heightScale = heightScale.copy().range(range0.map((d) => d + offset));\n _heightScale.clamp();\n }\n\n return {\n domain,\n pixelScale,\n pixelHeight,\n scale: _scale,\n // Internal details for hybrid scales. TODO: improve this\n heightScale: _heightScale,\n };\n}\n\nfunction findSectionHeightRange(\n data: UnitLong[],\n axisType: ColumnAxisType,\n): [number, number] {\n if (axisType == null) {\n throw new Error(\"Axis type is not set\");\n }\n if (axisType === ColumnAxisType.AGE) {\n const t_age = Math.min(...data.map((d) => d.t_age));\n const b_age = Math.max(...data.map((d) => d.b_age));\n return [b_age, t_age];\n } else if (\n axisType == ColumnAxisType.DEPTH ||\n axisType == ColumnAxisType.ORDINAL\n ) {\n const t_pos = Math.min(...data.map((d) => d.t_pos));\n const b_pos = Math.max(...data.map((d) => d.b_pos));\n return [b_pos, t_pos];\n } else if (axisType == ColumnAxisType.HEIGHT) {\n const t_pos = Math.max(...data.map((d) => d.t_pos));\n const b_pos = Math.min(...data.map((d) => d.b_pos));\n return [b_pos, t_pos];\n }\n}\n\nfunction visibleUnitExtents(\n data: UnitLong[],\n axisType: ColumnAxisType,\n visibleWindow?: [number, number] | null,\n): number[] {\n /** The durations of the units a density rule gets to size.\n *\n * Measured over what the render window actually *shows* — units outside it\n * are ignored and a unit it cuts through counts only for its visible part.\n * That's what makes `targetUnitHeight` mean the same thing at every zoom\n * depth: the units on screen are drawn at the target height whether they're\n * whole or slivers of something much longer.\n *\n * A section the window doesn't reach has nothing visible to measure, so it\n * falls back to its own units at full duration — it's drawn at its own scale.\n * (Deriving it from the padded window instead would be circular: padding is\n * measured in pixels, which is what we're trying to establish.)\n */\n const durations = (clip: boolean) =>\n data\n .map((d) => {\n const [b_pos, t_pos] = getUnitHeightRange(d, axisType);\n if (!clip) return Math.abs(b_pos - t_pos);\n const [b_win, t_win] = visibleWindow;\n return Math.max(0, Math.min(b_pos, b_win) - Math.max(t_pos, t_win));\n })\n .filter((d) => d > 0);\n\n const useWindow = visibleWindow != null && axisType === ColumnAxisType.AGE;\n let heights = useWindow ? durations(true) : [];\n if (heights.length === 0) heights = durations(false);\n return heights;\n}\n\nexport interface CompositeColumnScale {\n (age: number): number | null;\n copy(): CompositeColumnScale;\n domain(): [number, number];\n invert(pixelHeight: number): number | null;\n clamp(clamp: boolean): void;\n}\n\nexport function createCompositeScale(\n sections: PackageLayoutData[],\n interpolateUnconformities: boolean = false,\n): CompositeColumnScale {\n /** Create a scale that works across multiple packages */\n\n const scales: ScaleContinuousNumeric<number, number>[] = [];\n\n let lastScale: ScaleContinuousNumeric<number, number> | null = null;\n for (const section of sections) {\n const _scale = section.scaleInfo.scale.copy().clamp(true);\n scales.push(_scale);\n if (lastScale != null && interpolateUnconformities) {\n // Add a new scale that interpolates between lastScale and _scale\n const lastDomain = lastScale.domain();\n const lastRange = lastScale.range();\n const currentDomain = _scale.domain();\n const currentRange = _scale.range();\n\n const interpScale = scaleLinear()\n .domain([lastDomain[lastDomain.length - 1], currentDomain[0]])\n .range([lastRange[lastRange.length - 1], currentRange[0]])\n .clamp(true);\n scales.push(interpScale);\n }\n lastScale = _scale;\n }\n\n const scale: CompositeColumnScale = (age) => {\n for (const s of scales) {\n const domain = s.domain();\n if (\n domain[0] < domain[domain.length - 1] &&\n age >= domain[0] &&\n age <= domain[domain.length - 1]\n ) {\n // Age axes\n return s(age);\n } else if (age <= domain[0] && age >= domain[domain.length - 1]) {\n // Normal axes like height\n return s(age);\n }\n }\n return null;\n };\n\n scale.copy = () => {\n return createCompositeScale(sections, interpolateUnconformities);\n };\n\n scale.domain = () => {\n /** Return the domain of the scale */\n const vals = sections.flatMap((d) => d.scaleInfo.domain);\n if (vals[0] < vals[vals.length - 1]) {\n // age axes\n return [Math.min(...vals), Math.max(...vals)];\n }\n // Normal axes like height\n return [Math.max(...vals), Math.min(...vals)];\n };\n\n scale.invert = (pixelHeight) => {\n /** Invert the scale to get the age at a given pixel height */\n // Iterate through the sections to find the correct one\n for (const scale of scales) {\n const range = scale.range();\n if (\n pixelHeight > Math.min(...range) &&\n pixelHeight <= Math.max(...range)\n ) {\n //console.log(\"Inverting scale at pixel height\", pixelHeight);\n return scale.invert(pixelHeight);\n }\n }\n return null;\n };\n\n scale.clamp = (clamp: boolean) => {\n /** Clamp all constituent scales */\n for (const s of scales) {\n s.clamp(clamp);\n }\n };\n\n return scale;\n}\n\n/** Collapse sections separated by unconformities that are smaller than a given pixel height. */\nexport function collapseUnconformitiesByPixelHeight<T extends UnitLong>(\n sections: SectionInfoWithScale<T>[],\n threshold: number,\n opts: ColumnScaleOptions,\n): SectionInfoWithScale<T>[] {\n const newSections = [];\n let currentSection: SectionInfoWithScale<T> | null = null;\n for (const nextSection of sections) {\n if (currentSection == null) {\n currentSection = nextSection;\n continue;\n }\n let heights: [number, number];\n let pxHeights: [number, number];\n if (opts.axisType !== ColumnAxisType.AGE) {\n heights = [nextSection.t_pos, currentSection.b_pos];\n } else {\n heights = [nextSection.t_age, currentSection.b_age];\n }\n\n const _diff = (vals: number[]) => {\n return Math.abs(vals[0] - vals[1]);\n };\n\n pxHeights = [\n _diff(heights.map(nextSection.scaleInfo.scale)),\n _diff(heights.map(currentSection.scaleInfo.scale)),\n ];\n\n /** The gap has no density of its own — it falls between two sections that\n * may be drawn at very different ones — so it is judged at the finer of\n * the two. Taking the smaller estimate let a sparse neighbor speak for a\n * gap the other neighbor would have drawn many times larger: a 16 Myr\n * hiatus in column 22 read as 26px against one section and 166px against\n * the other, and collapsed. A gap is only worth hiding when neither scale\n * would give it more room than the break that replaces it. */\n const pxHeight = Math.max(...pxHeights);\n\n if (pxHeight < threshold) {\n let t_pos: number;\n let b_pos: number;\n if (\n opts.axisType == ColumnAxisType.AGE ||\n opts.axisType == ColumnAxisType.DEPTH\n ) {\n t_pos = Math.min(currentSection.t_pos, nextSection.t_pos);\n b_pos = Math.max(currentSection.b_pos, nextSection.b_pos);\n } else {\n t_pos = Math.max(currentSection.t_pos, nextSection.t_pos);\n b_pos = Math.min(currentSection.b_pos, nextSection.b_pos);\n }\n\n // We need to merge the sections\n const compositeSection0: SectionInfo<T> = {\n units: [...currentSection.units, ...nextSection.units],\n section_id: [\n ...ensureArray(currentSection.section_id),\n ...ensureArray(nextSection.section_id),\n ],\n t_age: Math.min(currentSection.t_age, nextSection.t_age),\n b_age: Math.max(currentSection.b_age, nextSection.b_age),\n t_pos,\n b_pos,\n };\n\n currentSection = addScaleToSection(compositeSection0, opts);\n } else {\n // We need to keep the section\n newSections.push(currentSection);\n currentSection = nextSection;\n }\n }\n if (currentSection != null) {\n newSections.push(currentSection);\n }\n\n return newSections;\n}\n"],"names":["scale"],"mappings":"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"}
@@ -1,11 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const columnComponents = require("@macrostrat/column-components");
4
- function unitHeight(px) {
5
- return (ctx) => px / typicalExtent(ctx.unitExtents);
6
- }
7
4
  function sectionHeight(px) {
8
- return (ctx) => px / ctx.extent;
5
+ return (ctx) => px / ctx.visibleExtent;
9
6
  }
10
7
  function fixedDensity(px) {
11
8
  return () => px;
@@ -25,14 +22,6 @@ function resolveDensity(rule, ctx) {
25
22
  if (typeof rule === "number") return rule;
26
23
  return rule(ctx);
27
24
  }
28
- function typicalExtent(extents) {
29
- const sorted = extents.filter((d) => d > 0).sort((a, b) => a - b);
30
- if (sorted.length === 0) return NaN;
31
- const mid = (sorted.length - 1) / 2;
32
- const lower = Math.floor(mid);
33
- const upper = Math.ceil(mid);
34
- return (sorted[lower] + sorted[upper]) / 2;
35
- }
36
25
  const DEFAULT_TARGET_UNIT_HEIGHT = 20;
37
26
  const DEFAULT_MIN_PIXEL_SCALE = 0.2;
38
27
  function sectionDensity(opts) {
@@ -54,7 +43,11 @@ function densityRule(opts) {
54
43
  minSectionHeight
55
44
  } = opts;
56
45
  const derived = atLeast(
57
- unitHeight(targetUnitHeight),
46
+ unitHeight(
47
+ targetUnitHeight,
48
+ "mean"
49
+ /* MEAN */
50
+ ),
58
51
  fixedDensity(minPixelScale),
59
52
  sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0)
60
53
  );
@@ -65,6 +58,21 @@ function densityRule(opts) {
65
58
  return derived(ctx);
66
59
  };
67
60
  }
61
+ function unitHeight(px, mode) {
62
+ return (ctx) => px / typicalExtent(ctx.unitExtents, mode);
63
+ }
64
+ function typicalExtent(extents, mode) {
65
+ const filtered = extents.filter((d) => d > 0);
66
+ if (filtered.length === 0) return NaN;
67
+ if (mode === "mean") {
68
+ return filtered.reduce((sum, val) => sum + val, 0) / filtered.length;
69
+ }
70
+ const sorted = filtered.sort((a, b) => a - b);
71
+ const mid = (sorted.length - 1) / 2;
72
+ const lower = Math.floor(mid);
73
+ const upper = Math.ceil(mid);
74
+ return (sorted[lower] + sorted[upper]) / 2;
75
+ }
68
76
  function statedDensity(opts, axisType) {
69
77
  const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;
70
78
  if (axisType === columnComponents.ColumnAxisType.AGE) {
@@ -1 +1 @@
1
- {"version":3,"file":"density.cjs","sources":["../../src/prepare-units/density.ts"],"sourcesContent":["/** How tall a column draws comes down to one quantity: **density**, the pixels\n * given to one unit of the axis — a Myr on an age column, a metre otherwise.\n *\n * Every option that sets a column's height names that quantity. Most are\n * floors, differing only in the units they're stated in: room for a unit,\n * for a section, or for the density itself. They combine by taking the\n * largest, so each says \"at least this much\", whichever ends up binding.\n */\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\nimport type { UnitLong } from \"@macrostrat/api-types\";\n\nexport interface SectionDensityContext {\n /** The section's extent, in axis units */\n extent: number;\n /** Extents of the units the render window actually shows, each clipped to\n * it. Measuring what's on screen is what makes a unit-height rule mean the\n * same thing at every zoom depth. */\n unitExtents: number[];\n /** The section's units, unclipped, for a rule that needs more than their\n * extents */\n units: UnitLong[];\n sectionID?: number;\n axisType: ColumnAxisType;\n}\n\n/** Pixels per axis unit, for one section */\nexport type SectionDensity = (ctx: SectionDensityContext) => number;\n\nexport type SectionDensityLike = number | SectionDensity;\n\n/** Room for a typical unit on screen: `px` tall, whatever its duration.\n *\n * \"Typical\" is the median of the visible extents, which is the only summary\n * that survives both tails. Unit durations are spread over orders of\n * magnitude: the arithmetic mean sits above any unit you would point at, so a\n * couple of long ones squeeze everything else below the target — and the\n * geometric mean fails the other way, since a single hair-thin unit (a\n * Holocene sliver beside a Pliocene terrace, say) drags the log-average down\n * and stretches the whole section to give that sliver its 20 pixels.\n */\nexport function unitHeight(px: number): SectionDensity {\n return (ctx) => px / typicalExtent(ctx.unitExtents);\n}\n\n/** Room for the section: `px` tall, whatever its extent. */\nexport function sectionHeight(px: number): SectionDensity {\n return (ctx) => px / ctx.extent;\n}\n\n/** A density stated outright, in pixels per axis unit. */\nexport function fixedDensity(px: number): SectionDensity {\n return () => px;\n}\n\n/** The rule that asks for the most room. Rules that don't resolve to a finite\n * number — a section of no extent, a window showing no units — are passed\n * over rather than swallowing the rest. */\nexport function atLeast(...rules: SectionDensityLike[]): SectionDensity {\n return (ctx) => {\n let density = 0;\n for (const rule of rules) {\n const value = resolveDensity(rule, ctx);\n if (!Number.isFinite(value)) continue;\n density = Math.max(density, value);\n }\n return density;\n };\n}\n\nexport function resolveDensity(\n rule: SectionDensityLike,\n ctx: SectionDensityContext,\n): number {\n if (typeof rule === \"number\") return rule;\n return rule(ctx);\n}\n\nfunction typicalExtent(extents: number[]): number {\n const sorted = extents.filter((d) => d > 0).sort((a, b) => a - b);\n if (sorted.length === 0) return NaN;\n const mid = (sorted.length - 1) / 2;\n const lower = Math.floor(mid);\n const upper = Math.ceil(mid);\n return (sorted[lower] + sorted[upper]) / 2;\n}\n\n/** Mirrors the default in `sectionDensity` */\nexport const DEFAULT_TARGET_UNIT_HEIGHT = 20;\n/** Small sections often have a unit or two, and no room for axis labels */\nconst DEFAULT_MIN_PIXEL_SCALE = 0.2;\n\n/** What a section is sized by. Stated for the column as a whole, and again\n * per section by `sectionOptions` where one needs different treatment. */\nexport interface SectionSizeOptions {\n /** A density stated outright, in pixels per axis unit: every section drawn\n * to the same scale whatever it holds. Rarely what you want within one\n * column, and the only way to make several of them comparable.\n *\n * Left out — the usual case — the density comes from the section's own\n * units, via `targetUnitHeight` and the floors. Set, it *is* the density:\n * the floors don't apply, since they would undo the scale you stated.\n *\n * It also takes a rule, for what the options here can't describe.\n *\n * This one is stated in whatever the axis measures, so it means a different\n * thing on an age axis than on a depth one. A view that switches between\n * them wants `pixelsPerMyr` and `pixelsPerMeter` instead. */\n pixelScale?: SectionDensityLike;\n /** Pixels per million years, on an age axis.\n *\n * The axis-specific spellings of `pixelScale`, and what to reach for when\n * the axis can change under you: useful values differ by orders of\n * magnitude between the two, so one number cannot serve both, any more than\n * `t_age` could serve as `t_pos`. Both can be held at once — the axis picks\n * — and either takes precedence over `pixelScale`. */\n pixelsPerMyr?: number;\n /** Pixels per metre, on a depth or height axis. Also the scale for an\n * approximate-height column, whose units are metres of measured thickness\n * however its axis is labeled. */\n pixelsPerMeter?: number;\n /** Room for a typical unit on screen. The knob that usually decides a\n * column, and the one to reach for to draw it larger or smaller. */\n targetUnitHeight?: number;\n /** Room for a section, however little it holds */\n minSectionHeight?: number;\n /** A floor on the density itself */\n minPixelScale?: number;\n}\n\n/** Sizing for one section, or a rule that works it out from what the section\n * holds. What it returns overrides the column-wide options for that section;\n * anything it leaves out keeps the column's value. */\nexport type SectionOptionsLike =\n | SectionSizeOptions\n | ((ctx: SectionDensityContext) => SectionSizeOptions);\n\nexport interface SectionDensityOptions extends SectionSizeOptions {\n /** Sizing decided per section */\n sectionOptions?: SectionOptionsLike;\n}\n\n/** The density rule a column uses, resolved per section so that\n * `sectionOptions` gets to see a section before saying how to draw it. */\nexport function sectionDensity(opts: SectionDensityOptions): SectionDensity {\n const { sectionOptions } = opts;\n if (sectionOptions == null) {\n const rule = densityRule(opts);\n return (ctx) => rule(ctx);\n }\n\n return (ctx) => {\n let overrides = sectionOptions;\n if (typeof sectionOptions === \"function\") overrides = sectionOptions(ctx);\n return densityRule({ ...opts, ...overrides })(ctx);\n };\n}\n\n/** The floors, combined: whichever asks for the most room wins. */\nfunction densityRule(opts: SectionSizeOptions): SectionDensity {\n const {\n targetUnitHeight = DEFAULT_TARGET_UNIT_HEIGHT,\n minPixelScale = DEFAULT_MIN_PIXEL_SCALE,\n minSectionHeight,\n } = opts;\n\n const derived = atLeast(\n unitHeight(targetUnitHeight),\n fixedDensity(minPixelScale),\n sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0),\n );\n\n return (ctx) => {\n // A density given outright answers for the section by itself. The floors\n // exist to keep a column readable when its own contents decide the\n // density; a scale you stated is a decision already made, and a floor\n // would quietly undo it — which is the whole point of stating one,\n // usually so that several columns can be compared.\n const stated = statedDensity(opts, ctx.axisType);\n if (typeof stated === \"function\") return stated(ctx);\n if (stated != null) return stated;\n return derived(ctx);\n };\n}\n\n/** The density this axis was given, if any. The axis-specific spellings win\n * over the generic one, which is what lets both be held while the axis\n * changes. */\nexport function statedDensity(\n opts: SectionSizeOptions,\n axisType: ColumnAxisType,\n): SectionDensityLike | undefined {\n const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;\n if (axisType === ColumnAxisType.AGE) {\n return pixelsPerMyr ?? pixelScale;\n }\n return pixelsPerMeter ?? pixelScale;\n}\n"],"names":["ColumnAxisType"],"mappings":";;;AAwCO,SAAS,WAAW,IAA4B;AACrD,SAAO,CAAC,QAAQ,KAAK,cAAc,IAAI,WAAW;AACpD;AAGO,SAAS,cAAc,IAA4B;AACxD,SAAO,CAAC,QAAQ,KAAK,IAAI;AAC3B;AAGO,SAAS,aAAa,IAA4B;AACvD,SAAO,MAAM;AACf;AAKO,SAAS,WAAW,OAA6C;AACtE,SAAO,CAAC,QAAQ;AACd,QAAI,UAAU;AACd,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,eAAe,MAAM,GAAG;AACtC,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG;AAC7B,gBAAU,KAAK,IAAI,SAAS,KAAK;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,eACd,MACA,KACQ;AACR,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,SAAO,KAAK,GAAG;AACjB;AAEA,SAAS,cAAc,SAA2B;AAChD,QAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAChE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,OAAO,OAAO,SAAS,KAAK;AAClC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAM,QAAQ,KAAK,KAAK,GAAG;AAC3B,UAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK;AAC3C;AAGO,MAAM,6BAA6B;AAE1C,MAAM,0BAA0B;AAsDzB,SAAS,eAAe,MAA6C;AAC1E,QAAM,EAAE,mBAAmB;AAC3B,MAAI,kBAAkB,MAAM;AAC1B,UAAM,OAAO,YAAY,IAAI;AAC7B,WAAO,CAAC,QAAQ,KAAK,GAAG;AAAA,EAC1B;AAEA,SAAO,CAAC,QAAQ;AACd,QAAI,YAAY;AAChB,QAAI,OAAO,mBAAmB,WAAY,aAAY,eAAe,GAAG;AACxE,WAAO,YAAY,EAAE,GAAG,MAAM,GAAG,UAAA,CAAW,EAAE,GAAG;AAAA,EACnD;AACF;AAGA,SAAS,YAAY,MAA0C;AAC7D,QAAM;AAAA,IACJ,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB;AAAA,EAAA,IACE;AAEJ,QAAM,UAAU;AAAA,IACd,WAAW,gBAAgB;AAAA,IAC3B,aAAa,aAAa;AAAA,IAC1B,cAAc,oBAAoB,oBAAoB,CAAC;AAAA,EAAA;AAGzD,SAAO,CAAC,QAAQ;AAMd,UAAM,SAAS,cAAc,MAAM,IAAI,QAAQ;AAC/C,QAAI,OAAO,WAAW,WAAY,QAAO,OAAO,GAAG;AACnD,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,QAAQ,GAAG;AAAA,EACpB;AACF;AAKO,SAAS,cACd,MACA,UACgC;AAChC,QAAM,EAAE,YAAY,cAAc,eAAA,IAAmB;AACrD,MAAI,aAAaA,iBAAAA,eAAe,KAAK;AACnC,WAAO,gBAAgB;AAAA,EACzB;AACA,SAAO,kBAAkB;AAC3B;;;;;;;;;"}
1
+ {"version":3,"file":"density.cjs","sources":["../../src/prepare-units/density.ts"],"sourcesContent":["/** How tall a column draws comes down to one quantity: **density**, the pixels\n * given to one unit of the axis — a Myr on an age column, a metre otherwise.\n *\n * Every option that sets a column's height names that quantity. Most are\n * floors, differing only in the units they're stated in: room for a unit,\n * for a section, or for the density itself. They combine by taking the\n * largest, so each says \"at least this much\", whichever ends up binding.\n */\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\nimport type { UnitLong } from \"@macrostrat/api-types\";\n\nexport interface SectionDensityContext {\n /** The section's extent, in axis units */\n extent: number;\n /** The section's shown extent, in axis units */\n visibleExtent: number;\n /** Extents of the units the render window actually shows, each clipped to\n * it. Measuring what's on screen is what makes a unit-height rule mean the\n * same thing at every zoom depth. */\n unitExtents: number[];\n /** The section's units, unclipped, for a rule that needs more than their\n * extents */\n units: UnitLong[];\n sectionID?: number;\n axisType: ColumnAxisType;\n}\n\n/** Pixels per axis unit, for one section */\nexport type SectionDensity = (ctx: SectionDensityContext) => number;\n\nexport type SectionDensityLike = number | SectionDensity;\n\nenum UnitHeightMode {\n MEDIAN = \"median\",\n MEAN = \"mean\",\n}\n\n/** Room for the section: `px` tall, whatever its extent. */\nexport function sectionHeight(px: number): SectionDensity {\n return (ctx) => px / ctx.visibleExtent;\n}\n\n/** A density stated outright, in pixels per axis unit. */\nexport function fixedDensity(px: number): SectionDensity {\n return () => px;\n}\n\n/** The rule that asks for the most room. Rules that don't resolve to a finite\n * number — a section of no extent, a window showing no units — are passed\n * over rather than swallowing the rest. */\nexport function atLeast(...rules: SectionDensityLike[]): SectionDensity {\n return (ctx) => {\n let density = 0;\n for (const rule of rules) {\n const value = resolveDensity(rule, ctx);\n if (!Number.isFinite(value)) continue;\n density = Math.max(density, value);\n }\n return density;\n };\n}\n\nexport function resolveDensity(\n rule: SectionDensityLike,\n ctx: SectionDensityContext,\n): number {\n if (typeof rule === \"number\") return rule;\n return rule(ctx);\n}\n\n/** Mirrors the default in `sectionDensity` */\nexport const DEFAULT_TARGET_UNIT_HEIGHT = 20;\n/** Small sections often have a unit or two, and no room for axis labels */\nconst DEFAULT_MIN_PIXEL_SCALE = 0.2;\n\n/** What a section is sized by. Stated for the column as a whole, and again\n * per section by `sectionOptions` where one needs different treatment. */\nexport interface SectionSizeOptions {\n /** A density stated outright, in pixels per axis unit: every section drawn\n * to the same scale whatever it holds. Rarely what you want within one\n * column, and the only way to make several of them comparable.\n *\n * Left out — the usual case — the density comes from the section's own\n * units, via `targetUnitHeight` and the floors. Set, it *is* the density:\n * the floors don't apply, since they would undo the scale you stated.\n *\n * It also takes a rule, for what the options here can't describe.\n *\n * This one is stated in whatever the axis measures, so it means a different\n * thing on an age axis than on a depth one. A view that switches between\n * them wants `pixelsPerMyr` and `pixelsPerMeter` instead. */\n pixelScale?: SectionDensityLike;\n /** Pixels per million years, on an age axis.\n *\n * The axis-specific spellings of `pixelScale`, and what to reach for when\n * the axis can change under you: useful values differ by orders of\n * magnitude between the two, so one number cannot serve both, any more than\n * `t_age` could serve as `t_pos`. Both can be held at once — the axis picks\n * — and either takes precedence over `pixelScale`. */\n pixelsPerMyr?: number;\n /** Pixels per metre, on a depth or height axis. Also the scale for an\n * approximate-height column, whose units are metres of measured thickness\n * however its axis is labeled. */\n pixelsPerMeter?: number;\n /** Room for a typical unit on screen. The knob that usually decides a\n * column, and the one to reach for to draw it larger or smaller. */\n targetUnitHeight?: number;\n /** Room for a section, however little it holds */\n minSectionHeight?: number;\n /** A floor on the density itself */\n minPixelScale?: number;\n}\n\n/** Sizing for one section, or a rule that works it out from what the section\n * holds. What it returns overrides the column-wide options for that section;\n * anything it leaves out keeps the column's value. */\nexport type SectionOptionsLike =\n SectionSizeOptions | ((ctx: SectionDensityContext) => SectionSizeOptions);\n\nexport interface SectionDensityOptions extends SectionSizeOptions {\n /** Sizing decided per section */\n sectionOptions?: SectionOptionsLike;\n}\n\n/** The density rule a column uses, resolved per section so that\n * `sectionOptions` gets to see a section before saying how to draw it. */\nexport function sectionDensity(opts: SectionDensityOptions): SectionDensity {\n const { sectionOptions } = opts;\n if (sectionOptions == null) {\n const rule = densityRule(opts);\n return (ctx) => rule(ctx);\n }\n\n return (ctx) => {\n let overrides = sectionOptions;\n if (typeof sectionOptions === \"function\") overrides = sectionOptions(ctx);\n return densityRule({ ...opts, ...overrides })(ctx);\n };\n}\n\n/** The floors, combined: whichever asks for the most room wins. */\nfunction densityRule(opts: SectionSizeOptions): SectionDensity {\n const {\n targetUnitHeight = DEFAULT_TARGET_UNIT_HEIGHT,\n minPixelScale = DEFAULT_MIN_PIXEL_SCALE,\n minSectionHeight,\n } = opts;\n\n const derived = atLeast(\n unitHeight(targetUnitHeight, UnitHeightMode.MEAN),\n fixedDensity(minPixelScale),\n sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0),\n );\n\n return (ctx) => {\n // A density given outright answers for the section by itself. The floors\n // exist to keep a column readable when its own contents decide the\n // density; a scale you stated is a decision already made, and a floor\n // would quietly undo it — which is the whole point of stating one,\n // usually so that several columns can be compared.\n const stated = statedDensity(opts, ctx.axisType);\n if (typeof stated === \"function\") return stated(ctx);\n if (stated != null) return stated;\n return derived(ctx);\n };\n}\n\n/** Room for a typical unit on screen: `px` tall, whatever its duration.\n *\n * \"Typical\" is the median of the visible extents, which is the only summary\n * that survives both tails. Unit durations are spread over orders of\n * magnitude: the arithmetic mean sits above any unit you would point at, so a\n * couple of long ones squeeze everything else below the target — and the\n * geometric mean fails the other way, since a single hair-thin unit (a\n * Holocene sliver beside a Pliocene terrace, say) drags the log-average down\n * and stretches the whole section to give that sliver its 20 pixels.\n */\nexport function unitHeight(px: number, mode: UnitHeightMode): SectionDensity {\n return (ctx) => px / typicalExtent(ctx.unitExtents, mode);\n}\n\nfunction typicalExtent(extents: number[], mode: UnitHeightMode): number {\n /** The typical extent for a unit, the median of its extents. */\n const filtered = extents.filter((d) => d > 0);\n if (filtered.length === 0) return NaN;\n if (mode === UnitHeightMode.MEAN) {\n return filtered.reduce((sum, val) => sum + val, 0) / filtered.length;\n }\n const sorted = filtered.sort((a, b) => a - b);\n const mid = (sorted.length - 1) / 2;\n const lower = Math.floor(mid);\n const upper = Math.ceil(mid);\n return (sorted[lower] + sorted[upper]) / 2;\n}\n\n/** The density this axis was given, if any. The axis-specific spellings win\n * over the generic one, which is what lets both be held while the axis\n * changes. */\nexport function statedDensity(\n opts: SectionSizeOptions,\n axisType: ColumnAxisType,\n): SectionDensityLike | undefined {\n const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;\n if (axisType === ColumnAxisType.AGE) {\n return pixelsPerMyr ?? pixelScale;\n }\n return pixelsPerMeter ?? pixelScale;\n}\n"],"names":["ColumnAxisType"],"mappings":";;;AAsCO,SAAS,cAAc,IAA4B;AACxD,SAAO,CAAC,QAAQ,KAAK,IAAI;AAC3B;AAGO,SAAS,aAAa,IAA4B;AACvD,SAAO,MAAM;AACf;AAKO,SAAS,WAAW,OAA6C;AACtE,SAAO,CAAC,QAAQ;AACd,QAAI,UAAU;AACd,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,eAAe,MAAM,GAAG;AACtC,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG;AAC7B,gBAAU,KAAK,IAAI,SAAS,KAAK;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,eACd,MACA,KACQ;AACR,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,SAAO,KAAK,GAAG;AACjB;AAGO,MAAM,6BAA6B;AAE1C,MAAM,0BAA0B;AAqDzB,SAAS,eAAe,MAA6C;AAC1E,QAAM,EAAE,mBAAmB;AAC3B,MAAI,kBAAkB,MAAM;AAC1B,UAAM,OAAO,YAAY,IAAI;AAC7B,WAAO,CAAC,QAAQ,KAAK,GAAG;AAAA,EAC1B;AAEA,SAAO,CAAC,QAAQ;AACd,QAAI,YAAY;AAChB,QAAI,OAAO,mBAAmB,WAAY,aAAY,eAAe,GAAG;AACxE,WAAO,YAAY,EAAE,GAAG,MAAM,GAAG,UAAA,CAAW,EAAE,GAAG;AAAA,EACnD;AACF;AAGA,SAAS,YAAY,MAA0C;AAC7D,QAAM;AAAA,IACJ,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB;AAAA,EAAA,IACE;AAEJ,QAAM,UAAU;AAAA,IACd;AAAA,MAAW;AAAA,MAAkB;AAAA;AAAA,IAAA;AAAA,IAC7B,aAAa,aAAa;AAAA,IAC1B,cAAc,oBAAoB,oBAAoB,CAAC;AAAA,EAAA;AAGzD,SAAO,CAAC,QAAQ;AAMd,UAAM,SAAS,cAAc,MAAM,IAAI,QAAQ;AAC/C,QAAI,OAAO,WAAW,WAAY,QAAO,OAAO,GAAG;AACnD,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,QAAQ,GAAG;AAAA,EACpB;AACF;AAYO,SAAS,WAAW,IAAY,MAAsC;AAC3E,SAAO,CAAC,QAAQ,KAAK,cAAc,IAAI,aAAa,IAAI;AAC1D;AAEA,SAAS,cAAc,SAAmB,MAA8B;AAEtE,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,IAAI,CAAC;AAC5C,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,MAAI,SAAS,QAAqB;AAChC,WAAO,SAAS,OAAO,CAAC,KAAK,QAAQ,MAAM,KAAK,CAAC,IAAI,SAAS;AAAA,EAChE;AACA,QAAM,SAAS,SAAS,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC5C,QAAM,OAAO,OAAO,SAAS,KAAK;AAClC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAM,QAAQ,KAAK,KAAK,GAAG;AAC3B,UAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK;AAC3C;AAKO,SAAS,cACd,MACA,UACgC;AAChC,QAAM,EAAE,YAAY,cAAc,eAAA,IAAmB;AACrD,MAAI,aAAaA,iBAAAA,eAAe,KAAK;AACnC,WAAO,gBAAgB;AAAA,EACzB;AACA,SAAO,kBAAkB;AAC3B;;;;;;;;;"}
@@ -3,6 +3,8 @@ import { UnitLong } from '@macrostrat/api-types';
3
3
  export interface SectionDensityContext {
4
4
  /** The section's extent, in axis units */
5
5
  extent: number;
6
+ /** The section's shown extent, in axis units */
7
+ visibleExtent: number;
6
8
  /** Extents of the units the render window actually shows, each clipped to
7
9
  * it. Measuring what's on screen is what makes a unit-height rule mean the
8
10
  * same thing at every zoom depth. */
@@ -16,17 +18,10 @@ export interface SectionDensityContext {
16
18
  /** Pixels per axis unit, for one section */
17
19
  export type SectionDensity = (ctx: SectionDensityContext) => number;
18
20
  export type SectionDensityLike = number | SectionDensity;
19
- /** Room for a typical unit on screen: `px` tall, whatever its duration.
20
- *
21
- * "Typical" is the median of the visible extents, which is the only summary
22
- * that survives both tails. Unit durations are spread over orders of
23
- * magnitude: the arithmetic mean sits above any unit you would point at, so a
24
- * couple of long ones squeeze everything else below the target — and the
25
- * geometric mean fails the other way, since a single hair-thin unit (a
26
- * Holocene sliver beside a Pliocene terrace, say) drags the log-average down
27
- * and stretches the whole section to give that sliver its 20 pixels.
28
- */
29
- export declare function unitHeight(px: number): SectionDensity;
21
+ declare enum UnitHeightMode {
22
+ MEDIAN = "median",
23
+ MEAN = "mean"
24
+ }
30
25
  /** Room for the section: `px` tall, whatever its extent. */
31
26
  export declare function sectionHeight(px: number): SectionDensity;
32
27
  /** A density stated outright, in pixels per axis unit. */
@@ -86,7 +81,19 @@ export interface SectionDensityOptions extends SectionSizeOptions {
86
81
  /** The density rule a column uses, resolved per section so that
87
82
  * `sectionOptions` gets to see a section before saying how to draw it. */
88
83
  export declare function sectionDensity(opts: SectionDensityOptions): SectionDensity;
84
+ /** Room for a typical unit on screen: `px` tall, whatever its duration.
85
+ *
86
+ * "Typical" is the median of the visible extents, which is the only summary
87
+ * that survives both tails. Unit durations are spread over orders of
88
+ * magnitude: the arithmetic mean sits above any unit you would point at, so a
89
+ * couple of long ones squeeze everything else below the target — and the
90
+ * geometric mean fails the other way, since a single hair-thin unit (a
91
+ * Holocene sliver beside a Pliocene terrace, say) drags the log-average down
92
+ * and stretches the whole section to give that sliver its 20 pixels.
93
+ */
94
+ export declare function unitHeight(px: number, mode: UnitHeightMode): SectionDensity;
89
95
  /** The density this axis was given, if any. The axis-specific spellings win
90
96
  * over the generic one, which is what lets both be held while the axis
91
97
  * changes. */
92
98
  export declare function statedDensity(opts: SectionSizeOptions, axisType: ColumnAxisType): SectionDensityLike | undefined;
99
+ export {};
@@ -1,9 +1,6 @@
1
1
  import { ColumnAxisType } from "@macrostrat/column-components";
2
- function unitHeight(px) {
3
- return (ctx) => px / typicalExtent(ctx.unitExtents);
4
- }
5
2
  function sectionHeight(px) {
6
- return (ctx) => px / ctx.extent;
3
+ return (ctx) => px / ctx.visibleExtent;
7
4
  }
8
5
  function fixedDensity(px) {
9
6
  return () => px;
@@ -23,14 +20,6 @@ function resolveDensity(rule, ctx) {
23
20
  if (typeof rule === "number") return rule;
24
21
  return rule(ctx);
25
22
  }
26
- function typicalExtent(extents) {
27
- const sorted = extents.filter((d) => d > 0).sort((a, b) => a - b);
28
- if (sorted.length === 0) return NaN;
29
- const mid = (sorted.length - 1) / 2;
30
- const lower = Math.floor(mid);
31
- const upper = Math.ceil(mid);
32
- return (sorted[lower] + sorted[upper]) / 2;
33
- }
34
23
  const DEFAULT_TARGET_UNIT_HEIGHT = 20;
35
24
  const DEFAULT_MIN_PIXEL_SCALE = 0.2;
36
25
  function sectionDensity(opts) {
@@ -52,7 +41,11 @@ function densityRule(opts) {
52
41
  minSectionHeight
53
42
  } = opts;
54
43
  const derived = atLeast(
55
- unitHeight(targetUnitHeight),
44
+ unitHeight(
45
+ targetUnitHeight,
46
+ "mean"
47
+ /* MEAN */
48
+ ),
56
49
  fixedDensity(minPixelScale),
57
50
  sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0)
58
51
  );
@@ -63,6 +56,21 @@ function densityRule(opts) {
63
56
  return derived(ctx);
64
57
  };
65
58
  }
59
+ function unitHeight(px, mode) {
60
+ return (ctx) => px / typicalExtent(ctx.unitExtents, mode);
61
+ }
62
+ function typicalExtent(extents, mode) {
63
+ const filtered = extents.filter((d) => d > 0);
64
+ if (filtered.length === 0) return NaN;
65
+ if (mode === "mean") {
66
+ return filtered.reduce((sum, val) => sum + val, 0) / filtered.length;
67
+ }
68
+ const sorted = filtered.sort((a, b) => a - b);
69
+ const mid = (sorted.length - 1) / 2;
70
+ const lower = Math.floor(mid);
71
+ const upper = Math.ceil(mid);
72
+ return (sorted[lower] + sorted[upper]) / 2;
73
+ }
66
74
  function statedDensity(opts, axisType) {
67
75
  const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;
68
76
  if (axisType === ColumnAxisType.AGE) {
@@ -1 +1 @@
1
- {"version":3,"file":"density.js","sources":["../../src/prepare-units/density.ts"],"sourcesContent":["/** How tall a column draws comes down to one quantity: **density**, the pixels\n * given to one unit of the axis — a Myr on an age column, a metre otherwise.\n *\n * Every option that sets a column's height names that quantity. Most are\n * floors, differing only in the units they're stated in: room for a unit,\n * for a section, or for the density itself. They combine by taking the\n * largest, so each says \"at least this much\", whichever ends up binding.\n */\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\nimport type { UnitLong } from \"@macrostrat/api-types\";\n\nexport interface SectionDensityContext {\n /** The section's extent, in axis units */\n extent: number;\n /** Extents of the units the render window actually shows, each clipped to\n * it. Measuring what's on screen is what makes a unit-height rule mean the\n * same thing at every zoom depth. */\n unitExtents: number[];\n /** The section's units, unclipped, for a rule that needs more than their\n * extents */\n units: UnitLong[];\n sectionID?: number;\n axisType: ColumnAxisType;\n}\n\n/** Pixels per axis unit, for one section */\nexport type SectionDensity = (ctx: SectionDensityContext) => number;\n\nexport type SectionDensityLike = number | SectionDensity;\n\n/** Room for a typical unit on screen: `px` tall, whatever its duration.\n *\n * \"Typical\" is the median of the visible extents, which is the only summary\n * that survives both tails. Unit durations are spread over orders of\n * magnitude: the arithmetic mean sits above any unit you would point at, so a\n * couple of long ones squeeze everything else below the target — and the\n * geometric mean fails the other way, since a single hair-thin unit (a\n * Holocene sliver beside a Pliocene terrace, say) drags the log-average down\n * and stretches the whole section to give that sliver its 20 pixels.\n */\nexport function unitHeight(px: number): SectionDensity {\n return (ctx) => px / typicalExtent(ctx.unitExtents);\n}\n\n/** Room for the section: `px` tall, whatever its extent. */\nexport function sectionHeight(px: number): SectionDensity {\n return (ctx) => px / ctx.extent;\n}\n\n/** A density stated outright, in pixels per axis unit. */\nexport function fixedDensity(px: number): SectionDensity {\n return () => px;\n}\n\n/** The rule that asks for the most room. Rules that don't resolve to a finite\n * number — a section of no extent, a window showing no units — are passed\n * over rather than swallowing the rest. */\nexport function atLeast(...rules: SectionDensityLike[]): SectionDensity {\n return (ctx) => {\n let density = 0;\n for (const rule of rules) {\n const value = resolveDensity(rule, ctx);\n if (!Number.isFinite(value)) continue;\n density = Math.max(density, value);\n }\n return density;\n };\n}\n\nexport function resolveDensity(\n rule: SectionDensityLike,\n ctx: SectionDensityContext,\n): number {\n if (typeof rule === \"number\") return rule;\n return rule(ctx);\n}\n\nfunction typicalExtent(extents: number[]): number {\n const sorted = extents.filter((d) => d > 0).sort((a, b) => a - b);\n if (sorted.length === 0) return NaN;\n const mid = (sorted.length - 1) / 2;\n const lower = Math.floor(mid);\n const upper = Math.ceil(mid);\n return (sorted[lower] + sorted[upper]) / 2;\n}\n\n/** Mirrors the default in `sectionDensity` */\nexport const DEFAULT_TARGET_UNIT_HEIGHT = 20;\n/** Small sections often have a unit or two, and no room for axis labels */\nconst DEFAULT_MIN_PIXEL_SCALE = 0.2;\n\n/** What a section is sized by. Stated for the column as a whole, and again\n * per section by `sectionOptions` where one needs different treatment. */\nexport interface SectionSizeOptions {\n /** A density stated outright, in pixels per axis unit: every section drawn\n * to the same scale whatever it holds. Rarely what you want within one\n * column, and the only way to make several of them comparable.\n *\n * Left out — the usual case — the density comes from the section's own\n * units, via `targetUnitHeight` and the floors. Set, it *is* the density:\n * the floors don't apply, since they would undo the scale you stated.\n *\n * It also takes a rule, for what the options here can't describe.\n *\n * This one is stated in whatever the axis measures, so it means a different\n * thing on an age axis than on a depth one. A view that switches between\n * them wants `pixelsPerMyr` and `pixelsPerMeter` instead. */\n pixelScale?: SectionDensityLike;\n /** Pixels per million years, on an age axis.\n *\n * The axis-specific spellings of `pixelScale`, and what to reach for when\n * the axis can change under you: useful values differ by orders of\n * magnitude between the two, so one number cannot serve both, any more than\n * `t_age` could serve as `t_pos`. Both can be held at once — the axis picks\n * — and either takes precedence over `pixelScale`. */\n pixelsPerMyr?: number;\n /** Pixels per metre, on a depth or height axis. Also the scale for an\n * approximate-height column, whose units are metres of measured thickness\n * however its axis is labeled. */\n pixelsPerMeter?: number;\n /** Room for a typical unit on screen. The knob that usually decides a\n * column, and the one to reach for to draw it larger or smaller. */\n targetUnitHeight?: number;\n /** Room for a section, however little it holds */\n minSectionHeight?: number;\n /** A floor on the density itself */\n minPixelScale?: number;\n}\n\n/** Sizing for one section, or a rule that works it out from what the section\n * holds. What it returns overrides the column-wide options for that section;\n * anything it leaves out keeps the column's value. */\nexport type SectionOptionsLike =\n | SectionSizeOptions\n | ((ctx: SectionDensityContext) => SectionSizeOptions);\n\nexport interface SectionDensityOptions extends SectionSizeOptions {\n /** Sizing decided per section */\n sectionOptions?: SectionOptionsLike;\n}\n\n/** The density rule a column uses, resolved per section so that\n * `sectionOptions` gets to see a section before saying how to draw it. */\nexport function sectionDensity(opts: SectionDensityOptions): SectionDensity {\n const { sectionOptions } = opts;\n if (sectionOptions == null) {\n const rule = densityRule(opts);\n return (ctx) => rule(ctx);\n }\n\n return (ctx) => {\n let overrides = sectionOptions;\n if (typeof sectionOptions === \"function\") overrides = sectionOptions(ctx);\n return densityRule({ ...opts, ...overrides })(ctx);\n };\n}\n\n/** The floors, combined: whichever asks for the most room wins. */\nfunction densityRule(opts: SectionSizeOptions): SectionDensity {\n const {\n targetUnitHeight = DEFAULT_TARGET_UNIT_HEIGHT,\n minPixelScale = DEFAULT_MIN_PIXEL_SCALE,\n minSectionHeight,\n } = opts;\n\n const derived = atLeast(\n unitHeight(targetUnitHeight),\n fixedDensity(minPixelScale),\n sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0),\n );\n\n return (ctx) => {\n // A density given outright answers for the section by itself. The floors\n // exist to keep a column readable when its own contents decide the\n // density; a scale you stated is a decision already made, and a floor\n // would quietly undo it — which is the whole point of stating one,\n // usually so that several columns can be compared.\n const stated = statedDensity(opts, ctx.axisType);\n if (typeof stated === \"function\") return stated(ctx);\n if (stated != null) return stated;\n return derived(ctx);\n };\n}\n\n/** The density this axis was given, if any. The axis-specific spellings win\n * over the generic one, which is what lets both be held while the axis\n * changes. */\nexport function statedDensity(\n opts: SectionSizeOptions,\n axisType: ColumnAxisType,\n): SectionDensityLike | undefined {\n const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;\n if (axisType === ColumnAxisType.AGE) {\n return pixelsPerMyr ?? pixelScale;\n }\n return pixelsPerMeter ?? pixelScale;\n}\n"],"names":[],"mappings":";AAwCO,SAAS,WAAW,IAA4B;AACrD,SAAO,CAAC,QAAQ,KAAK,cAAc,IAAI,WAAW;AACpD;AAGO,SAAS,cAAc,IAA4B;AACxD,SAAO,CAAC,QAAQ,KAAK,IAAI;AAC3B;AAGO,SAAS,aAAa,IAA4B;AACvD,SAAO,MAAM;AACf;AAKO,SAAS,WAAW,OAA6C;AACtE,SAAO,CAAC,QAAQ;AACd,QAAI,UAAU;AACd,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,eAAe,MAAM,GAAG;AACtC,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG;AAC7B,gBAAU,KAAK,IAAI,SAAS,KAAK;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,eACd,MACA,KACQ;AACR,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,SAAO,KAAK,GAAG;AACjB;AAEA,SAAS,cAAc,SAA2B;AAChD,QAAM,SAAS,QAAQ,OAAO,CAAC,MAAM,IAAI,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAChE,MAAI,OAAO,WAAW,EAAG,QAAO;AAChC,QAAM,OAAO,OAAO,SAAS,KAAK;AAClC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAM,QAAQ,KAAK,KAAK,GAAG;AAC3B,UAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK;AAC3C;AAGO,MAAM,6BAA6B;AAE1C,MAAM,0BAA0B;AAsDzB,SAAS,eAAe,MAA6C;AAC1E,QAAM,EAAE,mBAAmB;AAC3B,MAAI,kBAAkB,MAAM;AAC1B,UAAM,OAAO,YAAY,IAAI;AAC7B,WAAO,CAAC,QAAQ,KAAK,GAAG;AAAA,EAC1B;AAEA,SAAO,CAAC,QAAQ;AACd,QAAI,YAAY;AAChB,QAAI,OAAO,mBAAmB,WAAY,aAAY,eAAe,GAAG;AACxE,WAAO,YAAY,EAAE,GAAG,MAAM,GAAG,UAAA,CAAW,EAAE,GAAG;AAAA,EACnD;AACF;AAGA,SAAS,YAAY,MAA0C;AAC7D,QAAM;AAAA,IACJ,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB;AAAA,EAAA,IACE;AAEJ,QAAM,UAAU;AAAA,IACd,WAAW,gBAAgB;AAAA,IAC3B,aAAa,aAAa;AAAA,IAC1B,cAAc,oBAAoB,oBAAoB,CAAC;AAAA,EAAA;AAGzD,SAAO,CAAC,QAAQ;AAMd,UAAM,SAAS,cAAc,MAAM,IAAI,QAAQ;AAC/C,QAAI,OAAO,WAAW,WAAY,QAAO,OAAO,GAAG;AACnD,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,QAAQ,GAAG;AAAA,EACpB;AACF;AAKO,SAAS,cACd,MACA,UACgC;AAChC,QAAM,EAAE,YAAY,cAAc,eAAA,IAAmB;AACrD,MAAI,aAAa,eAAe,KAAK;AACnC,WAAO,gBAAgB;AAAA,EACzB;AACA,SAAO,kBAAkB;AAC3B;"}
1
+ {"version":3,"file":"density.js","sources":["../../src/prepare-units/density.ts"],"sourcesContent":["/** How tall a column draws comes down to one quantity: **density**, the pixels\n * given to one unit of the axis — a Myr on an age column, a metre otherwise.\n *\n * Every option that sets a column's height names that quantity. Most are\n * floors, differing only in the units they're stated in: room for a unit,\n * for a section, or for the density itself. They combine by taking the\n * largest, so each says \"at least this much\", whichever ends up binding.\n */\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\nimport type { UnitLong } from \"@macrostrat/api-types\";\n\nexport interface SectionDensityContext {\n /** The section's extent, in axis units */\n extent: number;\n /** The section's shown extent, in axis units */\n visibleExtent: number;\n /** Extents of the units the render window actually shows, each clipped to\n * it. Measuring what's on screen is what makes a unit-height rule mean the\n * same thing at every zoom depth. */\n unitExtents: number[];\n /** The section's units, unclipped, for a rule that needs more than their\n * extents */\n units: UnitLong[];\n sectionID?: number;\n axisType: ColumnAxisType;\n}\n\n/** Pixels per axis unit, for one section */\nexport type SectionDensity = (ctx: SectionDensityContext) => number;\n\nexport type SectionDensityLike = number | SectionDensity;\n\nenum UnitHeightMode {\n MEDIAN = \"median\",\n MEAN = \"mean\",\n}\n\n/** Room for the section: `px` tall, whatever its extent. */\nexport function sectionHeight(px: number): SectionDensity {\n return (ctx) => px / ctx.visibleExtent;\n}\n\n/** A density stated outright, in pixels per axis unit. */\nexport function fixedDensity(px: number): SectionDensity {\n return () => px;\n}\n\n/** The rule that asks for the most room. Rules that don't resolve to a finite\n * number — a section of no extent, a window showing no units — are passed\n * over rather than swallowing the rest. */\nexport function atLeast(...rules: SectionDensityLike[]): SectionDensity {\n return (ctx) => {\n let density = 0;\n for (const rule of rules) {\n const value = resolveDensity(rule, ctx);\n if (!Number.isFinite(value)) continue;\n density = Math.max(density, value);\n }\n return density;\n };\n}\n\nexport function resolveDensity(\n rule: SectionDensityLike,\n ctx: SectionDensityContext,\n): number {\n if (typeof rule === \"number\") return rule;\n return rule(ctx);\n}\n\n/** Mirrors the default in `sectionDensity` */\nexport const DEFAULT_TARGET_UNIT_HEIGHT = 20;\n/** Small sections often have a unit or two, and no room for axis labels */\nconst DEFAULT_MIN_PIXEL_SCALE = 0.2;\n\n/** What a section is sized by. Stated for the column as a whole, and again\n * per section by `sectionOptions` where one needs different treatment. */\nexport interface SectionSizeOptions {\n /** A density stated outright, in pixels per axis unit: every section drawn\n * to the same scale whatever it holds. Rarely what you want within one\n * column, and the only way to make several of them comparable.\n *\n * Left out — the usual case — the density comes from the section's own\n * units, via `targetUnitHeight` and the floors. Set, it *is* the density:\n * the floors don't apply, since they would undo the scale you stated.\n *\n * It also takes a rule, for what the options here can't describe.\n *\n * This one is stated in whatever the axis measures, so it means a different\n * thing on an age axis than on a depth one. A view that switches between\n * them wants `pixelsPerMyr` and `pixelsPerMeter` instead. */\n pixelScale?: SectionDensityLike;\n /** Pixels per million years, on an age axis.\n *\n * The axis-specific spellings of `pixelScale`, and what to reach for when\n * the axis can change under you: useful values differ by orders of\n * magnitude between the two, so one number cannot serve both, any more than\n * `t_age` could serve as `t_pos`. Both can be held at once — the axis picks\n * — and either takes precedence over `pixelScale`. */\n pixelsPerMyr?: number;\n /** Pixels per metre, on a depth or height axis. Also the scale for an\n * approximate-height column, whose units are metres of measured thickness\n * however its axis is labeled. */\n pixelsPerMeter?: number;\n /** Room for a typical unit on screen. The knob that usually decides a\n * column, and the one to reach for to draw it larger or smaller. */\n targetUnitHeight?: number;\n /** Room for a section, however little it holds */\n minSectionHeight?: number;\n /** A floor on the density itself */\n minPixelScale?: number;\n}\n\n/** Sizing for one section, or a rule that works it out from what the section\n * holds. What it returns overrides the column-wide options for that section;\n * anything it leaves out keeps the column's value. */\nexport type SectionOptionsLike =\n SectionSizeOptions | ((ctx: SectionDensityContext) => SectionSizeOptions);\n\nexport interface SectionDensityOptions extends SectionSizeOptions {\n /** Sizing decided per section */\n sectionOptions?: SectionOptionsLike;\n}\n\n/** The density rule a column uses, resolved per section so that\n * `sectionOptions` gets to see a section before saying how to draw it. */\nexport function sectionDensity(opts: SectionDensityOptions): SectionDensity {\n const { sectionOptions } = opts;\n if (sectionOptions == null) {\n const rule = densityRule(opts);\n return (ctx) => rule(ctx);\n }\n\n return (ctx) => {\n let overrides = sectionOptions;\n if (typeof sectionOptions === \"function\") overrides = sectionOptions(ctx);\n return densityRule({ ...opts, ...overrides })(ctx);\n };\n}\n\n/** The floors, combined: whichever asks for the most room wins. */\nfunction densityRule(opts: SectionSizeOptions): SectionDensity {\n const {\n targetUnitHeight = DEFAULT_TARGET_UNIT_HEIGHT,\n minPixelScale = DEFAULT_MIN_PIXEL_SCALE,\n minSectionHeight,\n } = opts;\n\n const derived = atLeast(\n unitHeight(targetUnitHeight, UnitHeightMode.MEAN),\n fixedDensity(minPixelScale),\n sectionHeight(minSectionHeight ?? targetUnitHeight ?? 0),\n );\n\n return (ctx) => {\n // A density given outright answers for the section by itself. The floors\n // exist to keep a column readable when its own contents decide the\n // density; a scale you stated is a decision already made, and a floor\n // would quietly undo it — which is the whole point of stating one,\n // usually so that several columns can be compared.\n const stated = statedDensity(opts, ctx.axisType);\n if (typeof stated === \"function\") return stated(ctx);\n if (stated != null) return stated;\n return derived(ctx);\n };\n}\n\n/** Room for a typical unit on screen: `px` tall, whatever its duration.\n *\n * \"Typical\" is the median of the visible extents, which is the only summary\n * that survives both tails. Unit durations are spread over orders of\n * magnitude: the arithmetic mean sits above any unit you would point at, so a\n * couple of long ones squeeze everything else below the target — and the\n * geometric mean fails the other way, since a single hair-thin unit (a\n * Holocene sliver beside a Pliocene terrace, say) drags the log-average down\n * and stretches the whole section to give that sliver its 20 pixels.\n */\nexport function unitHeight(px: number, mode: UnitHeightMode): SectionDensity {\n return (ctx) => px / typicalExtent(ctx.unitExtents, mode);\n}\n\nfunction typicalExtent(extents: number[], mode: UnitHeightMode): number {\n /** The typical extent for a unit, the median of its extents. */\n const filtered = extents.filter((d) => d > 0);\n if (filtered.length === 0) return NaN;\n if (mode === UnitHeightMode.MEAN) {\n return filtered.reduce((sum, val) => sum + val, 0) / filtered.length;\n }\n const sorted = filtered.sort((a, b) => a - b);\n const mid = (sorted.length - 1) / 2;\n const lower = Math.floor(mid);\n const upper = Math.ceil(mid);\n return (sorted[lower] + sorted[upper]) / 2;\n}\n\n/** The density this axis was given, if any. The axis-specific spellings win\n * over the generic one, which is what lets both be held while the axis\n * changes. */\nexport function statedDensity(\n opts: SectionSizeOptions,\n axisType: ColumnAxisType,\n): SectionDensityLike | undefined {\n const { pixelScale, pixelsPerMyr, pixelsPerMeter } = opts;\n if (axisType === ColumnAxisType.AGE) {\n return pixelsPerMyr ?? pixelScale;\n }\n return pixelsPerMeter ?? pixelScale;\n}\n"],"names":[],"mappings":";AAsCO,SAAS,cAAc,IAA4B;AACxD,SAAO,CAAC,QAAQ,KAAK,IAAI;AAC3B;AAGO,SAAS,aAAa,IAA4B;AACvD,SAAO,MAAM;AACf;AAKO,SAAS,WAAW,OAA6C;AACtE,SAAO,CAAC,QAAQ;AACd,QAAI,UAAU;AACd,eAAW,QAAQ,OAAO;AACxB,YAAM,QAAQ,eAAe,MAAM,GAAG;AACtC,UAAI,CAAC,OAAO,SAAS,KAAK,EAAG;AAC7B,gBAAU,KAAK,IAAI,SAAS,KAAK;AAAA,IACnC;AACA,WAAO;AAAA,EACT;AACF;AAEO,SAAS,eACd,MACA,KACQ;AACR,MAAI,OAAO,SAAS,SAAU,QAAO;AACrC,SAAO,KAAK,GAAG;AACjB;AAGO,MAAM,6BAA6B;AAE1C,MAAM,0BAA0B;AAqDzB,SAAS,eAAe,MAA6C;AAC1E,QAAM,EAAE,mBAAmB;AAC3B,MAAI,kBAAkB,MAAM;AAC1B,UAAM,OAAO,YAAY,IAAI;AAC7B,WAAO,CAAC,QAAQ,KAAK,GAAG;AAAA,EAC1B;AAEA,SAAO,CAAC,QAAQ;AACd,QAAI,YAAY;AAChB,QAAI,OAAO,mBAAmB,WAAY,aAAY,eAAe,GAAG;AACxE,WAAO,YAAY,EAAE,GAAG,MAAM,GAAG,UAAA,CAAW,EAAE,GAAG;AAAA,EACnD;AACF;AAGA,SAAS,YAAY,MAA0C;AAC7D,QAAM;AAAA,IACJ,mBAAmB;AAAA,IACnB,gBAAgB;AAAA,IAChB;AAAA,EAAA,IACE;AAEJ,QAAM,UAAU;AAAA,IACd;AAAA,MAAW;AAAA,MAAkB;AAAA;AAAA,IAAA;AAAA,IAC7B,aAAa,aAAa;AAAA,IAC1B,cAAc,oBAAoB,oBAAoB,CAAC;AAAA,EAAA;AAGzD,SAAO,CAAC,QAAQ;AAMd,UAAM,SAAS,cAAc,MAAM,IAAI,QAAQ;AAC/C,QAAI,OAAO,WAAW,WAAY,QAAO,OAAO,GAAG;AACnD,QAAI,UAAU,KAAM,QAAO;AAC3B,WAAO,QAAQ,GAAG;AAAA,EACpB;AACF;AAYO,SAAS,WAAW,IAAY,MAAsC;AAC3E,SAAO,CAAC,QAAQ,KAAK,cAAc,IAAI,aAAa,IAAI;AAC1D;AAEA,SAAS,cAAc,SAAmB,MAA8B;AAEtE,QAAM,WAAW,QAAQ,OAAO,CAAC,MAAM,IAAI,CAAC;AAC5C,MAAI,SAAS,WAAW,EAAG,QAAO;AAClC,MAAI,SAAS,QAAqB;AAChC,WAAO,SAAS,OAAO,CAAC,KAAK,QAAQ,MAAM,KAAK,CAAC,IAAI,SAAS;AAAA,EAChE;AACA,QAAM,SAAS,SAAS,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC;AAC5C,QAAM,OAAO,OAAO,SAAS,KAAK;AAClC,QAAM,QAAQ,KAAK,MAAM,GAAG;AAC5B,QAAM,QAAQ,KAAK,KAAK,GAAG;AAC3B,UAAQ,OAAO,KAAK,IAAI,OAAO,KAAK,KAAK;AAC3C;AAKO,SAAS,cACd,MACA,UACgC;AAChC,QAAM,EAAE,YAAY,cAAc,eAAA,IAAmB;AACrD,MAAI,aAAa,eAAe,KAAK;AACnC,WAAO,gBAAgB;AAAA,EACzB;AACA,SAAO,kBAAkB;AAC3B;"}
@@ -160,7 +160,7 @@ function groupUnitsIntoSectionsByOverlap(units, axisType = columnComponents.Colu
160
160
  break;
161
161
  }
162
162
  }
163
- return finalSectionList.map((section, i) => {
163
+ const s1 = finalSectionList.map((section, i) => {
164
164
  const [b_age, t_age] = getSectionAgeRange(section.units);
165
165
  const [b_pos, t_pos] = getSectionPosRange(section.units, axisType);
166
166
  return {
@@ -173,6 +173,7 @@ function groupUnitsIntoSectionsByOverlap(units, axisType = columnComponents.Colu
173
173
  units: section.units
174
174
  };
175
175
  });
176
+ return sortUnits(s1, axisType);
176
177
  }
177
178
  function getSectionPosRange(units, axisType) {
178
179
  const t_positions = units.map((d) => {
@@ -208,6 +209,20 @@ function getSectionPosRange(units, axisType) {
208
209
  function getSectionAgeRange(units) {
209
210
  return getSectionPosRange(units, columnComponents.ColumnAxisType.AGE);
210
211
  }
212
+ function sortUnits(units, axisType) {
213
+ return units.sort((a, b) => {
214
+ if (axisType === columnComponents.ColumnAxisType.HEIGHT && b.t_pos !== void 0 && a.t_pos !== void 0) {
215
+ return b.t_pos - a.t_pos;
216
+ }
217
+ if (axisType === columnComponents.ColumnAxisType.DEPTH && b.t_pos !== void 0 && a.t_pos !== void 0) {
218
+ return a.t_pos - b.t_pos;
219
+ }
220
+ if (axisType === columnComponents.ColumnAxisType.AGE) {
221
+ return a.t_age - b.t_age;
222
+ }
223
+ return 0;
224
+ });
225
+ }
211
226
  function mergeOverlappingSections(sections) {
212
227
  const [firstSection, ...rest] = sections;
213
228
  const newSections = [firstSection];
@@ -1 +1 @@
1
- {"version":3,"file":"helpers.cjs","sources":["../../src/prepare-units/helpers.ts"],"sourcesContent":["import type { BaseUnit, UnitLong } from \"@macrostrat/api-types\";\nimport { group } from \"d3-array\";\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\nimport {\n createUnitSorter,\n ensureArray,\n ensureRealFloat,\n getUnitHeightRange,\n unitsOverlap,\n} from \"./utils\";\nimport {\n AgeRangeRelationship,\n compareAgeRanges,\n mergeAgeRanges,\n MergeMode,\n} from \"@macrostrat/stratigraphy-utils\";\nimport type { ExtUnit, SectionInfo, StratigraphicPackage } from \"./types\";\n\nconst dt = 0.001;\n\nexport type UnitWithLayoutParameters<T extends BaseUnit> = T & {\n layout: UnitLayoutData<T>;\n};\n\nexport type UnitWithLayoutHints<T extends BaseUnit> = T & {\n layoutHints?: UnitLayoutHints;\n};\n\ninterface UnitLayoutInfo {\n column: number;\n nColumns: number;\n}\n\ntype UnitLayoutHints = Partial<UnitLayoutInfo>;\n\ntype UnitLayoutData<T extends BaseUnit> = UnitLayoutInfo & {\n totalColumns: number;\n unitGroup: UnitGroup<T>;\n};\n\nclass UnitGroup<T extends BaseUnit> implements StratigraphicPackage {\n units: T[];\n axisType: ColumnAxisType;\n\n constructor(units: T[], axisType: ColumnAxisType = ColumnAxisType.AGE) {\n this.units = units;\n this.axisType = axisType;\n }\n\n get t_age(): number {\n return Math.min(...this.units.map((d) => d.t_age));\n }\n\n get b_age(): number {\n return Math.max(...this.units.map((d) => d.b_age));\n }\n\n getUnits(): UnitWithLayoutParameters<T>[] {\n return assignColumnsToUnitsWithinOverlappingGroup(this);\n }\n}\n\nfunction groupUnitsByMutualOverlap<T extends BaseUnit>(\n units: T[],\n tolerance: number = dt,\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n): UnitGroup<T>[] {\n /** Group units within a section that have mutual overlap. */\n\n const sections = groupUnitsIntoSectionsByOverlap(units, axisType, tolerance);\n\n return sections.map((d) => {\n return new UnitGroup(d.units, axisType);\n });\n}\n\nconst sortByAge = createUnitSorter(ColumnAxisType.AGE);\n\nfunction assignColumnsToUnitsWithinOverlappingGroup<T extends BaseUnit>(\n group: UnitGroup<UnitWithLayoutHints<T>>,\n): UnitWithLayoutParameters<T>[] {\n /** A relatively naïve function to assign columns to units within a mutually overlapping group.\n * This gets the number of columns that would be needed to accommodate the units in the group.\n * It then assigns columns to units in the group based on the number of columns needed, with\n * lower units further to the left.\n */\n\n const sortedUnits = [...group.units].sort(createUnitSorter(group.axisType));\n\n const columns: T[][] = [];\n\n let nColumns = 1;\n\n for (const unit of sortedUnits) {\n let startIndex = 0;\n let endIndex = 0;\n // Find a column that is not already occupied by an overlapping unit\n const colHint = unit.layoutHints?.column;\n if (colHint != null) {\n columns[colHint] ??= [];\n columns[colHint].push(unit);\n continue;\n }\n let didAddToExistingColumn = false;\n\n for (const column of columns) {\n const lastUnit = column[column.length - 1];\n if (!unitsOverlap(unit, lastUnit, group.axisType, dt)) {\n column.push(unit);\n didAddToExistingColumn = true;\n break;\n }\n }\n if (!didAddToExistingColumn) {\n columns.push([unit]);\n }\n }\n\n const units: UnitWithLayoutParameters<T>[] = columns.flatMap(\n (column, columnIndex) =>\n column.map((unit) => {\n return {\n ...unit,\n layout: {\n column: columnIndex,\n nColumns: 1,\n totalColumns: columns.length,\n unitGroup: group,\n },\n };\n }),\n );\n\n return units.toSorted(sortByAge);\n}\n\nexport function preprocessUnits<T extends UnitLong = UnitLong>(\n section: SectionInfo<T>,\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n): ExtUnit[] {\n /** Preprocess units to add overlapping units and columns. */\n let units = section.units;\n\n let tolerance = 0.001; // 1 kyr tolerance for age columns\n if (axisType != ColumnAxisType.AGE) {\n tolerance = 0.01; // 1cm tolerance for height/depth columns\n }\n\n const unitGroups = groupUnitsByMutualOverlap(units, tolerance, axisType);\n\n const divisions: any[] = [];\n // Serialize all groups' units to a single array\n for (const group of unitGroups) {\n divisions.push(...group.getUnits());\n }\n\n return divisions.map((d) =>\n enhanceUnitWithClippingPosition(section, d, axisType),\n );\n}\n\nexport function enhanceUnitWithClippingPosition(section, d, axisType) {\n const [b_pos, t_pos] = getUnitHeightRange(d, axisType);\n if (axisType == ColumnAxisType.AGE) {\n if (b_pos > section.b_age) {\n d.b_clip_pos = section.b_age;\n }\n if (t_pos < section.t_age) {\n d.t_clip_pos = section.t_age;\n }\n }\n if (axisType == ColumnAxisType.DEPTH) {\n if (b_pos > section.b_pos) {\n d.b_clip_pos = section.b_pos;\n }\n if (t_pos < section.t_pos) {\n d.t_clip_pos = section.t_pos;\n }\n }\n return d;\n}\n\nexport function groupUnitsIntoSectionsBySectionID<T extends UnitLong>(\n units: T[],\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n): SectionInfo<T>[] {\n /** Group units into sections by section_id.\n * This works for large-scale Macrostrat columns, where units are grouped by section_id.\n * */\n let groups = Array.from(group(units, (d) => d.section_id));\n const unitComparator = createUnitSorter(axisType);\n\n const groups1 = groups.map(([section_id, sectionUnits]) => {\n const [b_age, t_age] = getSectionAgeRange(sectionUnits);\n const [b_pos, t_pos] = getSectionPosRange(sectionUnits, axisType);\n\n // sort units by position\n sectionUnits.sort(unitComparator);\n return { section_id, t_age, b_age, b_pos, t_pos, units: sectionUnits };\n });\n // Sort sections by increasing top age, then increasing bottom age.\n // Sections have no relative ordinal position other than age...\n const compareSections = createUnitSorter(axisType) as (\n a: StratigraphicPackage,\n b: StratigraphicPackage,\n ) => number;\n groups1.sort(compareSections);\n return groups1;\n}\n\ninterface WorkingSection {\n units: UnitLong[];\n // Position or age\n heightRange: [number, number];\n}\n\nexport function groupUnitsIntoSectionsByOverlap<T extends BaseUnit>(\n units: T[],\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n tolerance: number = dt,\n): SectionInfo<T>[] {\n /** Group units into sections by overlap.\n * This creates \"synthetic\" sections that correspond to packages bound by scale gaps.\n * This is most useful in the height and depth domains, where gaps (e.g., missing core) are\n * common.\n * */\n // Start with each unit as its own \"section\", and progressively merge...\n const sectionList: WorkingSection[] = [];\n\n const initialSections: WorkingSection[] = units.map((unit) => {\n return {\n units: [unit],\n heightRange: getUnitHeightRange(unit, axisType),\n };\n });\n\n const isOverlapping = (a: WorkingSection, b: WorkingSection) => {\n const res = compareAgeRanges(a.heightRange, b.heightRange, tolerance);\n return res !== AgeRangeRelationship.Disjoint;\n };\n\n const mergeSections = (a: WorkingSection, b: WorkingSection) => {\n return {\n units: [...a.units, ...b.units],\n heightRange: mergeAgeRanges(\n [a.heightRange, b.heightRange],\n MergeMode.Outer,\n ),\n };\n };\n\n // Merge overlapping sections progressively until no more merges are possible\n let finalSectionList = initialSections;\n\n while (true) {\n let merged = false;\n const nextSections: WorkingSection[] = [];\n\n for (const section of finalSectionList) {\n if (nextSections.length === 0) {\n nextSections.push(section);\n } else {\n const lastSection = nextSections[nextSections.length - 1];\n if (isOverlapping(section, lastSection)) {\n // Merge with the last section\n nextSections[nextSections.length - 1] = mergeSections(\n lastSection,\n section,\n );\n merged = true;\n } else {\n nextSections.push(section);\n }\n }\n }\n\n finalSectionList = nextSections;\n\n // Stop when no more merges occurred in this pass\n if (!merged) {\n break;\n }\n }\n\n return finalSectionList.map((section, i) => {\n const [b_age, t_age] = getSectionAgeRange(section.units);\n const [b_pos, t_pos] = getSectionPosRange(section.units, axisType);\n return {\n // Negative section IDs are used to indicate that these are synthetic sections\n section_id: -i,\n t_age,\n b_age,\n t_pos,\n b_pos,\n units: section.units as T[],\n };\n });\n}\n\nexport function getSectionPosRange(\n units: BaseUnit[],\n axisType: ColumnAxisType,\n): [number, number] {\n /** Get the overall position range of a set of units. */\n const t_positions = units.map((d) => {\n switch (axisType) {\n case ColumnAxisType.AGE:\n return d.t_age;\n case ColumnAxisType.DEPTH:\n case ColumnAxisType.HEIGHT:\n case ColumnAxisType.ORDINAL:\n return d.t_pos;\n default:\n throw new Error(`Unknown axis type: ${axisType}`);\n }\n });\n const b_positions = units.map((d) => {\n switch (axisType) {\n case ColumnAxisType.AGE:\n return d.b_age;\n case ColumnAxisType.DEPTH:\n case ColumnAxisType.HEIGHT:\n case ColumnAxisType.ORDINAL:\n return d.b_pos;\n default:\n throw new Error(`Unknown axis type: ${axisType}`);\n }\n });\n if (axisType == ColumnAxisType.AGE || axisType == ColumnAxisType.DEPTH) {\n return [Math.max(...b_positions), Math.min(...t_positions)];\n } else {\n return [Math.min(...b_positions), Math.max(...t_positions)];\n }\n}\n\nexport function getSectionAgeRange(units: BaseUnit[]): [number, number] {\n /** Get the overall age range of a set of units. */\n return getSectionPosRange(units, ColumnAxisType.AGE);\n}\n\nexport function mergeOverlappingSections<T extends UnitLong>(\n sections: SectionInfo<T>[],\n): SectionInfo<T>[] {\n /** Columns can have sections that overlap in time. Here, we merge overlapping\n * sections into a single section to correctly render gap-bound packages.\n */\n const [firstSection, ...rest] = sections;\n const newSections = [firstSection];\n for (const section of rest) {\n const lastSection = newSections[newSections.length - 1];\n if (\n lastSection.b_age < section.t_age ||\n lastSection.t_age > section.b_age\n ) {\n // No overlap, add the section as normal\n newSections.push(section);\n continue;\n }\n\n // Overlap, merge the sections\n lastSection.section_id = [\n ...ensureArray(lastSection.section_id),\n ...ensureArray(section.section_id),\n ];\n lastSection.units.push(...section.units);\n lastSection.b_age = Math.max(lastSection.b_age, section.b_age);\n lastSection.t_age = Math.min(lastSection.t_age, section.t_age);\n }\n return newSections;\n}\n\nexport function preprocessSectionUnit(unit: UnitLong): UnitLong {\n /** Preprocess a single unit for a \"section\" column type.\n * This mostly handles vagaries of eODP-style columns.\n * */\n\n let b_pos = unit.b_pos;\n let t_pos = unit.t_pos;\n\n let unit_name = unit.unit_name;\n\n // eODP columns sometimes have overlapping core sections, which are encoded in the name field\n // Match eODP section names\n const match = unit.unit_name.match(/^(\\d+(\\.\\d+)?)-(\\d+(\\.\\d+)?): (.+)/);\n\n if (match) {\n // These values should already be set if we've used the show_positions flag\n t_pos ??= ensureRealFloat(match[1]);\n b_pos ??= ensureRealFloat(match[3]);\n unit_name = match[5];\n }\n\n return {\n ...unit,\n b_pos: ensureRealFloat(b_pos),\n t_pos: ensureRealFloat(t_pos),\n unit_name,\n };\n}\n"],"names":["ColumnAxisType","createUnitSorter","group","unitsOverlap","getUnitHeightRange","compareAgeRanges","AgeRangeRelationship","mergeAgeRanges","MergeMode","ensureArray","ensureRealFloat"],"mappings":"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"}
1
+ {"version":3,"file":"helpers.cjs","sources":["../../src/prepare-units/helpers.ts"],"sourcesContent":["import type { BaseUnit, UnitLong } from \"@macrostrat/api-types\";\nimport { group } from \"d3-array\";\nimport { ColumnAxisType } from \"@macrostrat/column-components\";\nimport {\n createUnitSorter,\n ensureArray,\n ensureRealFloat,\n getUnitHeightRange,\n unitsOverlap,\n} from \"./utils\";\nimport {\n AgeRangeRelationship,\n compareAgeRanges,\n mergeAgeRanges,\n MergeMode,\n} from \"@macrostrat/stratigraphy-utils\";\nimport type { ExtUnit, SectionInfo, StratigraphicPackage } from \"./types\";\n\nconst dt = 0.001;\n\nexport type UnitWithLayoutParameters<T extends BaseUnit> = T & {\n layout: UnitLayoutData<T>;\n};\n\nexport type UnitWithLayoutHints<T extends BaseUnit> = T & {\n layoutHints?: UnitLayoutHints;\n};\n\ninterface UnitLayoutInfo {\n column: number;\n nColumns: number;\n}\n\ntype UnitLayoutHints = Partial<UnitLayoutInfo>;\n\ntype UnitLayoutData<T extends BaseUnit> = UnitLayoutInfo & {\n totalColumns: number;\n unitGroup: UnitGroup<T>;\n};\n\nclass UnitGroup<T extends BaseUnit> implements StratigraphicPackage {\n units: T[];\n axisType: ColumnAxisType;\n\n constructor(units: T[], axisType: ColumnAxisType = ColumnAxisType.AGE) {\n this.units = units;\n this.axisType = axisType;\n }\n\n get t_age(): number {\n return Math.min(...this.units.map((d) => d.t_age));\n }\n\n get b_age(): number {\n return Math.max(...this.units.map((d) => d.b_age));\n }\n\n getUnits(): UnitWithLayoutParameters<T>[] {\n return assignColumnsToUnitsWithinOverlappingGroup(this);\n }\n}\n\nfunction groupUnitsByMutualOverlap<T extends BaseUnit>(\n units: T[],\n tolerance: number = dt,\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n): UnitGroup<T>[] {\n /** Group units within a section that have mutual overlap. */\n\n const sections = groupUnitsIntoSectionsByOverlap(units, axisType, tolerance);\n\n return sections.map((d) => {\n return new UnitGroup(d.units, axisType);\n });\n}\n\nconst sortByAge = createUnitSorter(ColumnAxisType.AGE);\n\nfunction assignColumnsToUnitsWithinOverlappingGroup<T extends BaseUnit>(\n group: UnitGroup<UnitWithLayoutHints<T>>,\n): UnitWithLayoutParameters<T>[] {\n /** A relatively naïve function to assign columns to units within a mutually overlapping group.\n * This gets the number of columns that would be needed to accommodate the units in the group.\n * It then assigns columns to units in the group based on the number of columns needed, with\n * lower units further to the left.\n */\n\n const sortedUnits = [...group.units].sort(createUnitSorter(group.axisType));\n\n const columns: T[][] = [];\n\n let nColumns = 1;\n\n for (const unit of sortedUnits) {\n let startIndex = 0;\n let endIndex = 0;\n // Find a column that is not already occupied by an overlapping unit\n const colHint = unit.layoutHints?.column;\n if (colHint != null) {\n columns[colHint] ??= [];\n columns[colHint].push(unit);\n continue;\n }\n let didAddToExistingColumn = false;\n\n for (const column of columns) {\n const lastUnit = column[column.length - 1];\n if (!unitsOverlap(unit, lastUnit, group.axisType, dt)) {\n column.push(unit);\n didAddToExistingColumn = true;\n break;\n }\n }\n if (!didAddToExistingColumn) {\n columns.push([unit]);\n }\n }\n\n const units: UnitWithLayoutParameters<T>[] = columns.flatMap(\n (column, columnIndex) =>\n column.map((unit) => {\n return {\n ...unit,\n layout: {\n column: columnIndex,\n nColumns: 1,\n totalColumns: columns.length,\n unitGroup: group,\n },\n };\n }),\n );\n\n return units.toSorted(sortByAge);\n}\n\nexport function preprocessUnits<T extends UnitLong = UnitLong>(\n section: SectionInfo<T>,\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n): ExtUnit[] {\n /** Preprocess units to add overlapping units and columns. */\n let units = section.units;\n\n let tolerance = 0.001; // 1 kyr tolerance for age columns\n if (axisType != ColumnAxisType.AGE) {\n tolerance = 0.01; // 1cm tolerance for height/depth columns\n }\n\n const unitGroups = groupUnitsByMutualOverlap(units, tolerance, axisType);\n\n const divisions: any[] = [];\n // Serialize all groups' units to a single array\n for (const group of unitGroups) {\n divisions.push(...group.getUnits());\n }\n\n return divisions.map((d) =>\n enhanceUnitWithClippingPosition(section, d, axisType),\n );\n}\n\nexport function enhanceUnitWithClippingPosition(section, d, axisType) {\n const [b_pos, t_pos] = getUnitHeightRange(d, axisType);\n if (axisType == ColumnAxisType.AGE) {\n if (b_pos > section.b_age) {\n d.b_clip_pos = section.b_age;\n }\n if (t_pos < section.t_age) {\n d.t_clip_pos = section.t_age;\n }\n }\n if (axisType == ColumnAxisType.DEPTH) {\n if (b_pos > section.b_pos) {\n d.b_clip_pos = section.b_pos;\n }\n if (t_pos < section.t_pos) {\n d.t_clip_pos = section.t_pos;\n }\n }\n return d;\n}\n\nexport function groupUnitsIntoSectionsBySectionID<T extends UnitLong>(\n units: T[],\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n): SectionInfo<T>[] {\n /** Group units into sections by section_id.\n * This works for large-scale Macrostrat columns, where units are grouped by section_id.\n * */\n let groups = Array.from(group(units, (d) => d.section_id));\n const unitComparator = createUnitSorter(axisType);\n\n const groups1 = groups.map(([section_id, sectionUnits]) => {\n const [b_age, t_age] = getSectionAgeRange(sectionUnits);\n const [b_pos, t_pos] = getSectionPosRange(sectionUnits, axisType);\n\n // sort units by position\n sectionUnits.sort(unitComparator);\n return { section_id, t_age, b_age, b_pos, t_pos, units: sectionUnits };\n });\n // Sort sections by increasing top age, then increasing bottom age.\n // Sections have no relative ordinal position other than age...\n const compareSections = createUnitSorter(axisType) as (\n a: StratigraphicPackage,\n b: StratigraphicPackage,\n ) => number;\n groups1.sort(compareSections);\n return groups1;\n}\n\ninterface WorkingSection {\n units: UnitLong[];\n // Position or age\n heightRange: [number, number];\n}\n\nexport function groupUnitsIntoSectionsByOverlap<T extends BaseUnit>(\n units: T[],\n axisType: ColumnAxisType = ColumnAxisType.AGE,\n tolerance: number = dt,\n): SectionInfo<T>[] {\n /** Group units into sections by overlap.\n * This creates \"synthetic\" sections that correspond to packages bound by scale gaps.\n * This is most useful in the height and depth domains, where gaps (e.g., missing core) are\n * common.\n * */\n // Start with each unit as its own \"section\", and progressively merge...\n const sectionList: WorkingSection[] = [];\n\n const initialSections: WorkingSection[] = units.map((unit) => {\n return {\n units: [unit],\n heightRange: getUnitHeightRange(unit, axisType),\n };\n });\n\n const isOverlapping = (a: WorkingSection, b: WorkingSection) => {\n const res = compareAgeRanges(a.heightRange, b.heightRange, tolerance);\n return res !== AgeRangeRelationship.Disjoint;\n };\n\n const mergeSections = (a: WorkingSection, b: WorkingSection) => {\n return {\n units: [...a.units, ...b.units],\n heightRange: mergeAgeRanges(\n [a.heightRange, b.heightRange],\n MergeMode.Outer,\n ),\n };\n };\n\n // Merge overlapping sections progressively until no more merges are possible\n let finalSectionList = initialSections;\n\n while (true) {\n let merged = false;\n const nextSections: WorkingSection[] = [];\n\n for (const section of finalSectionList) {\n if (nextSections.length === 0) {\n nextSections.push(section);\n } else {\n const lastSection = nextSections[nextSections.length - 1];\n if (isOverlapping(section, lastSection)) {\n // Merge with the last section\n nextSections[nextSections.length - 1] = mergeSections(\n lastSection,\n section,\n );\n merged = true;\n } else {\n nextSections.push(section);\n }\n }\n }\n\n finalSectionList = nextSections;\n\n // Stop when no more merges occurred in this pass\n if (!merged) {\n break;\n }\n }\n\n const s1 = finalSectionList.map((section, i) => {\n const [b_age, t_age] = getSectionAgeRange(section.units);\n const [b_pos, t_pos] = getSectionPosRange(section.units, axisType);\n return {\n // Negative section IDs are used to indicate that these are synthetic sections\n section_id: -i,\n t_age,\n b_age,\n t_pos,\n b_pos,\n units: section.units as T[],\n };\n });\n\n return sortUnits(s1, axisType);\n}\n\nexport function getSectionPosRange(\n units: BaseUnit[],\n axisType: ColumnAxisType,\n): [number, number] {\n /** Get the overall position range of a set of units. */\n const t_positions = units.map((d) => {\n switch (axisType) {\n case ColumnAxisType.AGE:\n return d.t_age;\n case ColumnAxisType.DEPTH:\n case ColumnAxisType.HEIGHT:\n case ColumnAxisType.ORDINAL:\n return d.t_pos;\n default:\n throw new Error(`Unknown axis type: ${axisType}`);\n }\n });\n const b_positions = units.map((d) => {\n switch (axisType) {\n case ColumnAxisType.AGE:\n return d.b_age;\n case ColumnAxisType.DEPTH:\n case ColumnAxisType.HEIGHT:\n case ColumnAxisType.ORDINAL:\n return d.b_pos;\n default:\n throw new Error(`Unknown axis type: ${axisType}`);\n }\n });\n if (axisType == ColumnAxisType.AGE || axisType == ColumnAxisType.DEPTH) {\n return [Math.max(...b_positions), Math.min(...t_positions)];\n } else {\n return [Math.min(...b_positions), Math.max(...t_positions)];\n }\n}\n\nexport function getSectionAgeRange(units: BaseUnit[]): [number, number] {\n /** Get the overall age range of a set of units. */\n return getSectionPosRange(units, ColumnAxisType.AGE);\n}\n\nfunction forEachSection(fn, sections, ...args) {\n return sections.map((section) => {\n return {\n ...section,\n units: fn(section.units, ...args),\n };\n });\n}\n\nfunction sortUnits(units: BaseUnit[], axisType: ColumnAxisType) {\n return units.sort((a, b) => {\n if (\n axisType === ColumnAxisType.HEIGHT &&\n b.t_pos !== undefined &&\n a.t_pos !== undefined\n ) {\n return b.t_pos - a.t_pos;\n }\n if (\n axisType === ColumnAxisType.DEPTH &&\n b.t_pos !== undefined &&\n a.t_pos !== undefined\n ) {\n return a.t_pos - b.t_pos;\n }\n if (axisType === ColumnAxisType.AGE) {\n return a.t_age - b.t_age;\n }\n return 0;\n });\n}\n\nexport function mergeOverlappingSections<T extends UnitLong>(\n sections: SectionInfo<T>[],\n): SectionInfo<T>[] {\n /** Columns can have sections that overlap in time. Here, we merge overlapping\n * sections into a single section to correctly render gap-bound packages.\n */\n const [firstSection, ...rest] = sections;\n const newSections = [firstSection];\n for (const section of rest) {\n const lastSection = newSections[newSections.length - 1];\n if (\n lastSection.b_age < section.t_age ||\n lastSection.t_age > section.b_age\n ) {\n // No overlap, add the section as normal\n newSections.push(section);\n continue;\n }\n\n // Overlap, merge the sections\n lastSection.section_id = [\n ...ensureArray(lastSection.section_id),\n ...ensureArray(section.section_id),\n ];\n lastSection.units.push(...section.units);\n lastSection.b_age = Math.max(lastSection.b_age, section.b_age);\n lastSection.t_age = Math.min(lastSection.t_age, section.t_age);\n }\n return newSections;\n}\n\nexport function preprocessSectionUnit(unit: UnitLong): UnitLong {\n /** Preprocess a single unit for a \"section\" column type.\n * This mostly handles vagaries of eODP-style columns.\n * */\n\n let b_pos = unit.b_pos;\n let t_pos = unit.t_pos;\n\n let unit_name = unit.unit_name;\n\n // eODP columns sometimes have overlapping core sections, which are encoded in the name field\n // Match eODP section names\n const match = unit.unit_name.match(/^(\\d+(\\.\\d+)?)-(\\d+(\\.\\d+)?): (.+)/);\n\n if (match) {\n // These values should already be set if we've used the show_positions flag\n t_pos ??= ensureRealFloat(match[1]);\n b_pos ??= ensureRealFloat(match[3]);\n unit_name = match[5];\n }\n\n return {\n ...unit,\n b_pos: ensureRealFloat(b_pos),\n t_pos: ensureRealFloat(t_pos),\n unit_name,\n };\n}\n"],"names":["ColumnAxisType","createUnitSorter","group","unitsOverlap","getUnitHeightRange","compareAgeRanges","AgeRangeRelationship","mergeAgeRanges","MergeMode","ensureArray","ensureRealFloat"],"mappings":"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"}
@@ -158,7 +158,7 @@ function groupUnitsIntoSectionsByOverlap(units, axisType = ColumnAxisType.AGE, t
158
158
  break;
159
159
  }
160
160
  }
161
- return finalSectionList.map((section, i) => {
161
+ const s1 = finalSectionList.map((section, i) => {
162
162
  const [b_age, t_age] = getSectionAgeRange(section.units);
163
163
  const [b_pos, t_pos] = getSectionPosRange(section.units, axisType);
164
164
  return {
@@ -171,6 +171,7 @@ function groupUnitsIntoSectionsByOverlap(units, axisType = ColumnAxisType.AGE, t
171
171
  units: section.units
172
172
  };
173
173
  });
174
+ return sortUnits(s1, axisType);
174
175
  }
175
176
  function getSectionPosRange(units, axisType) {
176
177
  const t_positions = units.map((d) => {
@@ -206,6 +207,20 @@ function getSectionPosRange(units, axisType) {
206
207
  function getSectionAgeRange(units) {
207
208
  return getSectionPosRange(units, ColumnAxisType.AGE);
208
209
  }
210
+ function sortUnits(units, axisType) {
211
+ return units.sort((a, b) => {
212
+ if (axisType === ColumnAxisType.HEIGHT && b.t_pos !== void 0 && a.t_pos !== void 0) {
213
+ return b.t_pos - a.t_pos;
214
+ }
215
+ if (axisType === ColumnAxisType.DEPTH && b.t_pos !== void 0 && a.t_pos !== void 0) {
216
+ return a.t_pos - b.t_pos;
217
+ }
218
+ if (axisType === ColumnAxisType.AGE) {
219
+ return a.t_age - b.t_age;
220
+ }
221
+ return 0;
222
+ });
223
+ }
209
224
  function mergeOverlappingSections(sections) {
210
225
  const [firstSection, ...rest] = sections;
211
226
  const newSections = [firstSection];