@macrostrat/column-views 3.11.0 → 3.12.1

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.
Files changed (138) hide show
  1. package/CHANGELOG.md +184 -0
  2. package/dist/age-axis.cjs +1 -2
  3. package/dist/age-axis.cjs.map +1 -1
  4. package/dist/age-axis.js +1 -2
  5. package/dist/age-axis.js.map +1 -1
  6. package/dist/age-model/data.cjs +57 -0
  7. package/dist/age-model/data.cjs.map +1 -0
  8. package/dist/age-model/data.d.ts +29 -0
  9. package/dist/age-model/data.js +57 -0
  10. package/dist/age-model/data.js.map +1 -0
  11. package/dist/age-model/details.cjs +260 -0
  12. package/dist/age-model/details.cjs.map +1 -0
  13. package/dist/age-model/details.d.ts +39 -0
  14. package/dist/age-model/details.js +257 -0
  15. package/dist/age-model/details.js.map +1 -0
  16. package/dist/age-model/details.module.sass.cjs +42 -0
  17. package/dist/age-model/details.module.sass.cjs.map +1 -0
  18. package/dist/age-model/details.module.sass.js +40 -0
  19. package/dist/age-model/details.module.sass.js.map +1 -0
  20. package/dist/age-model/index.d.ts +5 -0
  21. package/dist/age-model/legacy.cjs +28 -0
  22. package/dist/age-model/legacy.cjs.map +1 -0
  23. package/dist/age-model/legacy.d.ts +6 -0
  24. package/dist/age-model/legacy.js +26 -0
  25. package/dist/age-model/legacy.js.map +1 -0
  26. package/dist/age-model/surfaces.cjs +354 -0
  27. package/dist/age-model/surfaces.cjs.map +1 -0
  28. package/dist/age-model/surfaces.d.ts +68 -0
  29. package/dist/age-model/surfaces.js +351 -0
  30. package/dist/age-model/surfaces.js.map +1 -0
  31. package/dist/age-model/surfaces.module.sass.cjs +45 -0
  32. package/dist/age-model/surfaces.module.sass.cjs.map +1 -0
  33. package/dist/age-model/surfaces.module.sass.js +43 -0
  34. package/dist/age-model/surfaces.module.sass.js.map +1 -0
  35. package/dist/age-model/types.cjs +229 -0
  36. package/dist/age-model/types.cjs.map +1 -0
  37. package/dist/age-model/types.d.ts +112 -0
  38. package/dist/age-model/types.js +229 -0
  39. package/dist/age-model/types.js.map +1 -0
  40. package/dist/column-views.css +406 -74
  41. package/dist/column.cjs +20 -13
  42. package/dist/column.cjs.map +1 -1
  43. package/dist/column.d.ts +21 -6
  44. package/dist/column.js +21 -14
  45. package/dist/column.js.map +1 -1
  46. package/dist/column.module.sass.cjs +19 -15
  47. package/dist/column.module.sass.cjs.map +1 -1
  48. package/dist/column.module.sass.js +19 -15
  49. package/dist/column.module.sass.js.map +1 -1
  50. package/dist/data-provider/store.cjs +34 -1
  51. package/dist/data-provider/store.cjs.map +1 -1
  52. package/dist/data-provider/store.d.ts +5 -0
  53. package/dist/data-provider/store.js +35 -2
  54. package/dist/data-provider/store.js.map +1 -1
  55. package/dist/index.cjs +78 -18
  56. package/dist/index.cjs.map +1 -1
  57. package/dist/index.d.ts +3 -1
  58. package/dist/index.js +55 -4
  59. package/dist/index.js.map +1 -1
  60. package/dist/notes.cjs +11 -2
  61. package/dist/notes.cjs.map +1 -1
  62. package/dist/notes.d.ts +11 -1
  63. package/dist/notes.js +11 -2
  64. package/dist/notes.js.map +1 -1
  65. package/dist/prepare-units/composite-scale.cjs +18 -16
  66. package/dist/prepare-units/composite-scale.cjs.map +1 -1
  67. package/dist/prepare-units/composite-scale.js +18 -16
  68. package/dist/prepare-units/composite-scale.js.map +1 -1
  69. package/dist/prepare-units/density.cjs +83 -0
  70. package/dist/prepare-units/density.cjs.map +1 -0
  71. package/dist/prepare-units/density.d.ts +92 -0
  72. package/dist/prepare-units/density.js +83 -0
  73. package/dist/prepare-units/density.js.map +1 -0
  74. package/dist/prepare-units/index.cjs +15 -7
  75. package/dist/prepare-units/index.cjs.map +1 -1
  76. package/dist/prepare-units/index.d.ts +1 -0
  77. package/dist/prepare-units/index.js +15 -7
  78. package/dist/prepare-units/index.js.map +1 -1
  79. package/dist/prepare-units/types.cjs.map +1 -1
  80. package/dist/prepare-units/types.d.ts +34 -13
  81. package/dist/prepare-units/types.js.map +1 -1
  82. package/dist/section.cjs +108 -24
  83. package/dist/section.cjs.map +1 -1
  84. package/dist/section.d.ts +66 -6
  85. package/dist/section.js +109 -25
  86. package/dist/section.js.map +1 -1
  87. package/dist/timescale-zoom.cjs +191 -0
  88. package/dist/timescale-zoom.cjs.map +1 -0
  89. package/dist/timescale-zoom.d.ts +74 -0
  90. package/dist/timescale-zoom.js +191 -0
  91. package/dist/timescale-zoom.js.map +1 -0
  92. package/dist/unit-details/age-range.cjs +14 -43
  93. package/dist/unit-details/age-range.cjs.map +1 -1
  94. package/dist/unit-details/age-range.d.ts +1 -13
  95. package/dist/unit-details/age-range.js +6 -43
  96. package/dist/unit-details/age-range.js.map +1 -1
  97. package/dist/unit-details/utils.cjs +0 -5
  98. package/dist/unit-details/utils.cjs.map +1 -1
  99. package/dist/unit-details/utils.js +0 -5
  100. package/dist/unit-details/utils.js.map +1 -1
  101. package/dist/units/composite.cjs +4 -1
  102. package/dist/units/composite.cjs.map +1 -1
  103. package/dist/units/composite.d.ts +2 -0
  104. package/dist/units/composite.js +4 -1
  105. package/dist/units/composite.js.map +1 -1
  106. package/package.json +8 -8
  107. package/src/age-model/data.ts +98 -0
  108. package/src/age-model/details.module.sass +108 -0
  109. package/src/age-model/details.ts +333 -0
  110. package/src/age-model/index.ts +5 -0
  111. package/src/age-model/legacy.ts +30 -0
  112. package/src/age-model/surfaces.module.sass +168 -0
  113. package/src/age-model/surfaces.ts +568 -0
  114. package/src/age-model/types.ts +392 -0
  115. package/src/column.module.sass +59 -8
  116. package/src/column.ts +49 -23
  117. package/src/data-provider/store.ts +50 -1
  118. package/src/index.ts +5 -1
  119. package/src/notes.ts +20 -2
  120. package/src/prepare-units/composite-scale.ts +34 -31
  121. package/src/prepare-units/density.ts +197 -0
  122. package/src/prepare-units/index.ts +25 -6
  123. package/src/prepare-units/types.ts +34 -13
  124. package/src/section.ts +219 -33
  125. package/src/timescale-zoom.ts +341 -0
  126. package/src/unit-details/age-range.ts +12 -58
  127. package/src/units/composite.ts +11 -0
  128. package/dist/age-model-overlay.cjs +0 -50
  129. package/dist/age-model-overlay.cjs.map +0 -1
  130. package/dist/age-model-overlay.d.ts +0 -24
  131. package/dist/age-model-overlay.js +0 -48
  132. package/dist/age-model-overlay.js.map +0 -1
  133. package/dist/age-model-overlay.module.sass.cjs +0 -12
  134. package/dist/age-model-overlay.module.sass.cjs.map +0 -1
  135. package/dist/age-model-overlay.module.sass.js +0 -11
  136. package/dist/age-model-overlay.module.sass.js.map +0 -1
  137. package/src/age-model-overlay.module.sass +0 -11
  138. package/src/age-model-overlay.ts +0 -96
@@ -1 +1 @@
1
- {"version":3,"file":"section.js","sources":["../src/section.ts"],"sourcesContent":["import {\n CompositeUnitsColumn,\n LabelTrackerProvider,\n SectionLabelsColumn,\n} from \"./units\";\nimport { ReactNode, FunctionComponent, useMemo } from \"react\";\nimport {\n Timescale,\n TimescaleOrientation,\n TimescaleClickHandler,\n useMacrostratIntervals,\n type IntervalStyleBuilder,\n} from \"@macrostrat/timescale\";\nimport { ColumnAxisType, SVG } from \"@macrostrat/column-components\";\nimport hyper from \"@macrostrat/hyper\";\nimport styles from \"./column.module.sass\";\nimport {\n useMacrostratColumnData,\n useMacrostratUnits,\n MacrostratColumnProvider,\n} from \"./data-provider\";\nimport {\n useMacrostratBaseURL,\n useMacrostratData,\n} from \"@macrostrat/data-provider\";\nimport { Duration } from \"./unit-details\";\nimport { Value } from \"@macrostrat/data-components\";\nimport type { ExtUnit, PackageScaleLayoutData } from \"./prepare-units/types\";\n\nconst h = hyper.styled(styles);\n\nexport interface SectionSharedProps {\n unitComponent?: FunctionComponent<any>;\n unitComponentProps?: any;\n showLabels?: boolean;\n width?: number;\n columnWidth?: number;\n children?: ReactNode;\n showLabelColumn?: boolean;\n axisType?: ColumnAxisType;\n className?: string;\n clipUnits?: boolean;\n maxInternalColumns?: number;\n // Space between sections\n verticalSpacing?: number;\n // Whether to render unconformity labels\n unconformityLabels?: boolean;\n}\n\nexport interface SectionProps extends SectionSharedProps {\n units: ExtUnit[];\n scaleInfo: PackageScaleLayoutData;\n}\n\nexport function SectionsColumn(props: SectionSharedProps) {\n const {\n unconformityLabels = true,\n unitComponent,\n unitComponentProps,\n showLabels = true,\n width = 300,\n columnWidth = 150,\n showLabelColumn = true,\n clipUnits = true,\n maxInternalColumns,\n } = props;\n\n const units = useMacrostratUnits();\n\n // Get a unique key for the column\n const key = units[0]?.col_id;\n\n return h(LabelTrackerProvider, { units, key }, [\n h(SectionUnitsColumn, {\n width: columnWidth,\n unitComponent,\n unitComponentProps,\n showLabels,\n clipUnits,\n maxInternalColumns,\n unconformityLabels,\n }),\n h.if(showLabelColumn)(SectionLabelsColumn, {\n width: width - columnWidth,\n }),\n ]);\n}\n\nfunction SectionUnitsColumn(props: SectionSharedProps) {\n const {\n width,\n showLabels,\n unitComponent,\n unitComponentProps,\n clipUnits,\n maxInternalColumns,\n axisType,\n unconformityLabels = true,\n } = props;\n\n const { sections, totalHeight } = useMacrostratColumnData();\n\n const scaleData: PackageScaleLayoutData[] = sections.map((section) => {\n return section.scaleInfo;\n });\n\n const innerWidth = width - 2;\n\n return h(\"div.section-units-container\", { style: { width } }, [\n h(\n SVG,\n {\n className: \"sections\",\n height: totalHeight,\n innerWidth,\n paddingH: 1,\n },\n sections.map((group) => {\n const { units, scaleInfo, section_id } = group;\n\n const key = `section-${section_id}`;\n return h(\n SectionUnits,\n {\n units,\n scaleInfo,\n key,\n unitComponent,\n unitComponentProps,\n showLabels,\n width: innerWidth,\n clipUnits,\n maxInternalColumns,\n }, // This unconformity is with the section _above_\n );\n }),\n ),\n h.if(unconformityLabels)(UnconformityLabels, {\n width,\n sections: scaleData,\n verbose: false,\n }),\n ]);\n}\n\nfunction SectionUnits(props: SectionProps) {\n // Section with \"squishy\" timescale\n const {\n units,\n scaleInfo,\n unitComponent,\n showLabels = true,\n width = 300,\n unitComponentProps,\n className,\n clipUnits = true,\n maxInternalColumns,\n } = props;\n\n const { domain, pixelScale, pixelHeight, scale } = scaleInfo;\n\n /** Ensure that we can arrange units into the maximum number\n * of columns defined by unitComponentProps, but that we don't\n * use more than necessary.\n */\n const _unitComponentProps = useMemo(() => {\n return {\n ...unitComponentProps,\n nColumns: Math.min(\n maxInternalColumns ?? Math.floor(width / 10),\n unitComponentProps?.nColumns ?? Infinity,\n ),\n //axisType,\n };\n }, [units, unitComponentProps, maxInternalColumns, width]);\n\n const style = {\n \"--section-height\": `${pixelHeight}px`,\n \"--section-width\": `${width}px`,\n };\n\n return h(\n \"g.section\",\n { className, style, transform: `translate(0 ${scaleInfo.offset})` },\n h(\n MacrostratColumnProvider,\n {\n units,\n domain,\n pixelScale, // Actually pixels per myr,\n scale,\n },\n h(CompositeUnitsColumn, {\n width,\n showLabels,\n unitComponent,\n unitComponentProps: _unitComponentProps,\n clipToFrame: clipUnits,\n }),\n ),\n );\n}\n\ninterface CompositeTimescaleProps {\n levels?: [number, number] | number;\n unconformityLabels?: boolean;\n onClickInterval?: TimescaleClickHandler;\n /** Per-interval style (e.g. to highlight the currently selected interval). */\n intervalStyle?: IntervalStyleBuilder;\n}\n\nexport function CompositeTimescale(props: CompositeTimescaleProps) {\n const { sections } = useMacrostratColumnData();\n const sectionScales = sections.map((section) => {\n return section.scaleInfo;\n });\n\n return h(CompositeTimescaleCore, {\n packages: sectionScales,\n ...props,\n });\n}\n\ntype CompositeTimescaleCoreProps = CompositeTimescaleProps & {\n packages: PackageScaleLayoutData[];\n onClickInterval?: TimescaleClickHandler;\n};\n\nexport function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {\n const {\n levels = 3,\n packages,\n unconformityLabels = false,\n onClickInterval,\n intervalStyle,\n } = props;\n\n // Use intervals from Macrostrat API\n const baseURL = useMacrostratBaseURL();\n const intervals = useMacrostratIntervals({ baseURL });\n\n let _levels: [number, number];\n if (typeof levels === \"number\") {\n // If levels is a number, use the most common starting level\n _levels = [2, Math.max(2 + Math.min(levels, 5) - 1, 1)];\n } else {\n _levels = levels;\n }\n\n const nCols = _levels[1] - _levels[0] + 1;\n\n return h(\"div.timescale-column\", [\n h(\n \"div.timescales\",\n packages.map((group) => {\n const { pixelHeight, paddingTop, key, scale } = group;\n return h(\n \"div.timescale-container\",\n { style: { paddingTop, \"--timescale-level-count\": nCols }, key },\n [\n h(Timescale, {\n orientation: TimescaleOrientation.VERTICAL,\n length: pixelHeight,\n levels: _levels,\n absoluteAgeScale: true,\n showAgeAxis: false,\n scale,\n intervals,\n onClick: onClickInterval,\n intervalStyle,\n }),\n ],\n );\n }),\n ),\n h.if(unconformityLabels)(UnconformityLabels, {\n width: \"100%\",\n sections: packages,\n className: \"unconformity-labels\",\n axisType: ColumnAxisType.AGE,\n }),\n ]);\n}\n\nexport function UnconformityLabels(props: {\n width: string | number;\n sections: PackageScaleLayoutData[];\n axisType?: ColumnAxisType;\n className?: string;\n verbose?: boolean;\n}) {\n const { width, sections, className, axisType, verbose } = props;\n\n return h(\n \"div.unconformity-labels\",\n {\n style: {\n width,\n },\n className,\n },\n sections.map((scaleInfo, i) => {\n const lastGroup = sections[i - 1];\n const top = scaleInfo.offset - scaleInfo.paddingTop;\n const upperAge = lastGroup?.domain[0];\n const lowerAge = scaleInfo.domain[1];\n return h(Unconformity, {\n axisType,\n upperAge,\n lowerAge,\n style: {\n width,\n height: scaleInfo.paddingTop,\n top,\n },\n verbose,\n });\n }),\n );\n}\n\nfunction Unconformity({\n upperAge,\n lowerAge,\n style,\n axisType,\n verbose = false,\n}) {\n if (upperAge == null || lowerAge == null) {\n return null;\n }\n\n const ageGap = Math.abs(upperAge - lowerAge);\n\n let maximumFractionDigits = 0;\n\n let className: string = null;\n if (ageGap > 1000) {\n className = \"giga\";\n } else if (ageGap > 100) {\n className = \"mega\";\n } else if (ageGap > 10) {\n className = \"large\";\n } else if (ageGap < 1) {\n className = \"small\";\n maximumFractionDigits = 2;\n } else {\n maximumFractionDigits = 1;\n }\n\n let val: ReactNode;\n if (axisType === ColumnAxisType.DEPTH || axisType === ColumnAxisType.HEIGHT) {\n const _txt = ageGap.toLocaleString(\"en-US\", { maximumFractionDigits });\n val = h(Value, { value: _txt, unit: \"m\" });\n } else {\n val = h(Duration, { value: ageGap, maximumFractionDigits });\n }\n\n let prefix: ReactNode = null;\n if (verbose) {\n prefix = h([\" \", h(\"span.prefix\", \" gap\")]);\n }\n\n return h(\"div.unconformity\", { style, className }, [\n h(\"div.unconformity-inner\", h(\"div.unconformity-text\", [val, prefix])),\n ]);\n}\n"],"names":["hyper","styles"],"mappings":"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"}
1
+ {"version":3,"file":"section.js","sources":["../src/section.ts"],"sourcesContent":["import {\n CompositeUnitsColumn,\n LabelTrackerProvider,\n SectionLabelsColumn,\n} from \"./units\";\nimport {\n type CSSProperties,\n ReactNode,\n FunctionComponent,\n useMemo,\n} from \"react\";\nimport {\n Timescale,\n TimescaleOrientation,\n useMacrostratTimescales,\n type Interval,\n type IntervalStyleBuilder,\n type TimescaleClickData,\n} from \"@macrostrat/timescale\";\n\n/** Macrostrat's international timescale, the default for a column */\nconst INTERNATIONAL_TIMESCALE_ID = 11;\nimport { ColumnAxisType, SVG } from \"@macrostrat/column-components\";\nimport hyper from \"@macrostrat/hyper\";\nimport styles from \"./column.module.sass\";\nimport {\n useMacrostratColumnData,\n useMacrostratUnits,\n MacrostratColumnProvider,\n} from \"./data-provider\";\nimport { Duration } from \"./unit-details\";\nimport { Value } from \"@macrostrat/data-components\";\nimport type { ExtUnit, PackageScaleLayoutData } from \"./prepare-units/types\";\n\nconst h = hyper.styled(styles);\n\nexport interface SectionSharedProps {\n unitComponent?: FunctionComponent<any>;\n unitComponentProps?: any;\n showLabels?: boolean;\n width?: number;\n columnWidth?: number;\n children?: ReactNode;\n showLabelColumn?: boolean;\n /** Suppress the label for a unit drawn thinner than this many pixels */\n labelSuppressHeight?: number;\n axisType?: ColumnAxisType;\n className?: string;\n clipUnits?: boolean;\n maxInternalColumns?: number;\n // Space between sections\n verticalSpacing?: number;\n // Whether to render unconformity labels\n unconformityLabels?: boolean;\n}\n\nexport interface SectionProps extends SectionSharedProps {\n units: ExtUnit[];\n scaleInfo: PackageScaleLayoutData;\n}\n\nexport function SectionsColumn(props: SectionSharedProps) {\n const {\n unconformityLabels = true,\n unitComponent,\n unitComponentProps,\n showLabels = true,\n width = 300,\n columnWidth = 150,\n showLabelColumn = true,\n clipUnits = true,\n maxInternalColumns,\n labelSuppressHeight,\n } = props;\n\n const units = useMacrostratUnits();\n\n // Get a unique key for the column\n const key = units[0]?.col_id;\n\n return h(LabelTrackerProvider, { units, key }, [\n h(SectionUnitsColumn, {\n width: columnWidth,\n unitComponent,\n unitComponentProps,\n showLabels,\n clipUnits,\n maxInternalColumns,\n unconformityLabels,\n }),\n h.if(showLabelColumn)(SectionLabelsColumn, {\n width: width - columnWidth,\n labelSuppressHeight,\n }),\n ]);\n}\n\nfunction SectionUnitsColumn(props: SectionSharedProps) {\n const {\n width,\n showLabels,\n unitComponent,\n unitComponentProps,\n clipUnits,\n maxInternalColumns,\n axisType,\n unconformityLabels = true,\n } = props;\n\n const { sections, totalHeight, axisType: columnAxisType } =\n useMacrostratColumnData();\n\n const scaleData: PackageScaleLayoutData[] = sections.map((section) => {\n return section.scaleInfo;\n });\n\n const innerWidth = width - 2;\n\n return h(\"div.section-units-container\", { style: { width } }, [\n h(\n SVG,\n {\n className: \"sections\",\n height: totalHeight,\n innerWidth,\n paddingH: 1,\n },\n sections.map((group) => {\n const { units, scaleInfo, section_id } = group;\n\n const key = `section-${section_id}`;\n return h(\n SectionUnits,\n {\n units,\n scaleInfo,\n key,\n unitComponent,\n unitComponentProps,\n showLabels,\n width: innerWidth,\n clipUnits,\n maxInternalColumns,\n }, // This unconformity is with the section _above_\n );\n }),\n ),\n h.if(unconformityLabels)(UnconformityLabels, {\n width,\n sections: scaleData,\n // A gap between sections is measured in whatever the axis is: metres\n // down a core, Myr across a time column\n axisType: axisType ?? columnAxisType,\n verbose: false,\n }),\n ]);\n}\n\nfunction SectionUnits(props: SectionProps) {\n // Section with \"squishy\" timescale\n const {\n units,\n scaleInfo,\n unitComponent,\n showLabels = true,\n width = 300,\n unitComponentProps,\n className,\n clipUnits = true,\n maxInternalColumns,\n } = props;\n\n const { domain, pixelScale, pixelHeight, scale } = scaleInfo;\n\n /** Ensure that we can arrange units into the maximum number\n * of columns defined by unitComponentProps, but that we don't\n * use more than necessary.\n */\n const _unitComponentProps = useMemo(() => {\n return {\n ...unitComponentProps,\n nColumns: Math.min(\n maxInternalColumns ?? Math.floor(width / 10),\n unitComponentProps?.nColumns ?? Infinity,\n ),\n //axisType,\n };\n }, [units, unitComponentProps, maxInternalColumns, width]);\n\n const style = {\n \"--section-height\": `${pixelHeight}px`,\n \"--section-width\": `${width}px`,\n };\n\n return h(\n \"g.section\",\n { className, style, transform: `translate(0 ${scaleInfo.offset})` },\n h(\n MacrostratColumnProvider,\n {\n units,\n domain,\n pixelScale, // Actually pixels per myr,\n scale,\n },\n h(CompositeUnitsColumn, {\n width,\n showLabels,\n unitComponent,\n unitComponentProps: _unitComponentProps,\n clipToFrame: clipUnits,\n }),\n ),\n );\n}\n\n/** A per-interval style that also knows which timescale the interval was\n * drawn from. The same interval often appears in several of the timescales\n * beside each other, and they aren't interchangeable — only one of them is\n * the one a selection was made in. */\nexport type ColumnIntervalStyleBuilder =\n | CSSProperties\n | ((interval: Interval, timescaleID: number) => CSSProperties)\n | null;\n\n/** A timescale click, reporting which timescale was clicked in */\nexport type ColumnTimescaleClickHandler = (\n event: Event,\n data: TimescaleClickData & { timescaleID: number },\n) => void;\n\n/** A timescale to draw beside a column.\n *\n * The international timescale is one of these, not a special case: it is the\n * one that happens to be nested several levels deep. Anything with the same\n * shape can take its place or sit beside it — a regional set from the API, a\n * zonation assembled locally, or a hierarchy that isn't a timescale at all,\n * so long as its \"intervals\" carry ages.\n */\nexport interface ColumnTimescale {\n /** Integer identifier. Clicks and per-interval styles report it, so a\n * consumer can tell one column's copy of an interval from another's. For a\n * Macrostrat timescale it is the `timescale_id`. */\n id: number;\n /** Shown above the column, unless `label` says otherwise */\n name?: string;\n /** The intervals themselves, flat or already nested by `pid`. Left out,\n * the timescale `id` is fetched from the Macrostrat API. */\n intervals?: Interval[];\n /** Levels to draw, and so how many slots wide the column is (default\n * `[1, 1]`: one level, which is what a flat timescale has) */\n levels?: [number, number];\n /** `oid` of the interval the tree hangs from (default 0) */\n rootInterval?: number;\n /** Drawn above the column in place of the name — any node, so a consumer\n * can put a control or a legend there instead of text */\n label?: ReactNode;\n}\n\nexport type ColumnTimescaleLike = number | ColumnTimescale;\n\ninterface CompositeTimescaleProps {\n /** Every timescale to draw, in order. A bare number is a Macrostrat\n * timescale to fetch and draw as a single level.\n *\n * Given this, `levels`, `timescaleID` and `additionalTimescales` are\n * ignored: it says what they say and more. */\n timescales?: ColumnTimescaleLike[];\n /** Levels of the ICS hierarchy to show. If a number, the finest level is\n * `levels[0]`, the second-finest is `levels[1]`, etc. If an array, the\n * first element is the starting level, the second is the ending level. */\n levels?: [number, number] | number;\n unconformityLabels?: boolean;\n onClickInterval?: ColumnTimescaleClickHandler;\n /** Per-interval style (e.g. to highlight the currently selected interval). */\n intervalStyle?: ColumnIntervalStyleBuilder;\n /** The timescale to draw. Defaults to the international one; any other\n * Macrostrat timescale (a regional or project-specific set of intervals)\n * can be drawn in its place. */\n timescaleID?: number;\n /** Further timescales drawn as extra level columns beside the main one,\n * against the same scale. Shorthand for listing them in `timescales`. */\n additionalTimescales?: number[];\n /** Draw the names above the columns. They sit above the column proper, so\n * leave top padding for them. */\n showLabels?: boolean;\n className?: string;\n}\n\nexport function CompositeTimescale(props: CompositeTimescaleProps) {\n const { sections } = useMacrostratColumnData();\n const sectionScales = sections.map((section) => {\n return section.scaleInfo;\n });\n\n return h(CompositeTimescaleCore, {\n packages: sectionScales,\n ...props,\n });\n}\n\ntype CompositeTimescaleCoreProps = CompositeTimescaleProps & {\n packages: PackageScaleLayoutData[];\n};\n\n/** What is drawn above a timescale: its name, reading up the page, or\n * whatever node the consumer gave instead. */\nfunction TimescaleLabel({ timescale }: { timescale: ColumnTimescale }) {\n const { label, name } = timescale;\n const style = levelCountStyle(levelCount(timescale));\n\n let content: ReactNode = label;\n if (label == null && name != null) {\n content = h(\"span.timescale-label-text\", name);\n }\n if (content == null) return h(\"div.timescale-label\", { style });\n\n return h(\"div.timescale-label\", { style }, content);\n}\n\n/** How wide a timescale draws: one slot per level it shows. */\nfunction levelCountStyle(levelCount: number): CSSProperties {\n return { \"--timescale-level-count\": levelCount } as CSSProperties;\n}\n\nexport function CompositeTimescaleCore(props: CompositeTimescaleCoreProps) {\n const {\n packages,\n unconformityLabels = false,\n onClickInterval,\n intervalStyle,\n showLabels = false,\n className,\n } = props;\n\n const timescales = useColumnTimescales(props);\n\n /** Clicks and styles carry the timescale they came from, so a consumer can\n * tell one column's copy of an interval from another's. */\n function forTimescale(id: number) {\n let onClick = null;\n if (onClickInterval != null) {\n onClick = (event: Event, data: TimescaleClickData) => {\n onClickInterval(event, { ...data, timescaleID: id });\n };\n }\n let style = intervalStyle as IntervalStyleBuilder;\n if (typeof intervalStyle === \"function\") {\n style = (interval: Interval) => intervalStyle(interval, id);\n }\n return { onClick, intervalStyle: style };\n }\n\n return h(\"div.timescale-column\", { className }, [\n h.if(showLabels)(\n \"div.timescale-labels\",\n timescales.map((timescale) =>\n h(TimescaleLabel, { key: timescale.id, timescale }),\n ),\n ),\n h(\n \"div.timescales\",\n packages.map((group) => {\n const { pixelHeight, paddingTop, key, scale } = group;\n return h(\n \"div.section-timescales\",\n { style: { paddingTop }, key },\n timescales.map((timescale) =>\n h(Timescale, {\n key: timescale.id,\n orientation: TimescaleOrientation.VERTICAL,\n length: pixelHeight,\n levels: timescale.levels,\n rootInterval: timescale.rootInterval,\n absoluteAgeScale: true,\n showAgeAxis: false,\n scale,\n intervals: timescale.intervals,\n style: levelCountStyle(levelCount(timescale)),\n ...forTimescale(timescale.id),\n }),\n ),\n );\n }),\n ),\n h.if(unconformityLabels)(UnconformityLabels, {\n width: \"100%\",\n sections: packages,\n className: \"unconformity-labels\",\n axisType: ColumnAxisType.AGE,\n }),\n ]);\n}\n\n/** The timescales to draw, with their intervals.\n *\n * Every timescale a column draws is fetched in one place: a column is split\n * into sections, and fetching per section would ask for the same timescale\n * once per section per timescale.\n */\nfunction useColumnTimescales(props: CompositeTimescaleProps): ColumnTimescale[] {\n const { timescales, levels = 3, timescaleID, additionalTimescales } = props;\n\n const specs = useMemo(\n () => timescaleSpecs({ timescales, levels, timescaleID, additionalTimescales }),\n [\n timescales,\n levels,\n timescaleID,\n additionalTimescales?.join(\",\"),\n ],\n );\n\n // Only the ones that didn't come with their own intervals need fetching\n const fetchIDs = useMemo(\n () => specs.filter((d) => d.intervals == null).map((d) => d.id),\n [specs],\n );\n const intervalSets = useMacrostratTimescales(fetchIDs);\n\n return useMemo(\n () =>\n specs.map((spec) => ({\n ...spec,\n intervals: spec.intervals ?? intervalSets.get(spec.id) ?? [],\n })),\n [specs, intervalSets],\n );\n}\n\n/** The `timescales` list, or one built from the older props that name the\n * leveled timescale and the flat ones beside it. */\nfunction timescaleSpecs(props: CompositeTimescaleProps): ColumnTimescale[] {\n const { timescales, levels = 3, timescaleID, additionalTimescales = [] } = props;\n\n if (timescales != null) {\n return timescales.map(normalizeTimescale);\n }\n\n return [\n {\n id: timescaleID ?? INTERNATIONAL_TIMESCALE_ID,\n levels: levelRange(levels),\n },\n ...additionalTimescales.map(normalizeTimescale),\n ];\n}\n\nfunction normalizeTimescale(timescale: ColumnTimescaleLike): ColumnTimescale {\n if (typeof timescale === \"number\") {\n return { id: timescale, levels: FLAT_TIMESCALE_LEVELS };\n }\n return { levels: FLAT_TIMESCALE_LEVELS, ...timescale };\n}\n\n/** Regional and project timescales come back flat: one level of intervals. */\nconst FLAT_TIMESCALE_LEVELS: [number, number] = [1, 1];\n\nfunction levelRange(levels: [number, number] | number): [number, number] {\n if (typeof levels !== \"number\") return levels;\n // A count, rather than a range: start at the level most columns start at\n return [2, Math.max(2 + Math.min(levels, 5) - 1, 1)];\n}\n\n/** How many slots wide a timescale draws: one per level it shows. */\nfunction levelCount(timescale: ColumnTimescale): number {\n const [min, max] = timescale.levels ?? FLAT_TIMESCALE_LEVELS;\n return Math.max(max - min + 1, 1);\n}\n\nexport function UnconformityLabels(props: {\n width: string | number;\n sections: PackageScaleLayoutData[];\n axisType?: ColumnAxisType;\n className?: string;\n verbose?: boolean;\n}) {\n const { width, sections, className, axisType, verbose } = props;\n\n return h(\n \"div.unconformity-labels\",\n {\n style: {\n width,\n },\n className,\n },\n sections.map((scaleInfo, i) => {\n const lastGroup = sections[i - 1];\n const top = scaleInfo.offset - scaleInfo.paddingTop;\n const upperAge = lastGroup?.domain[0];\n const lowerAge = scaleInfo.domain[1];\n return h(Unconformity, {\n axisType,\n upperAge,\n lowerAge,\n style: {\n width,\n height: scaleInfo.paddingTop,\n top,\n },\n verbose,\n });\n }),\n );\n}\n\nfunction Unconformity({\n upperAge,\n lowerAge,\n style,\n axisType,\n verbose = false,\n}) {\n if (upperAge == null || lowerAge == null) {\n return null;\n }\n\n const ageGap = Math.abs(upperAge - lowerAge);\n\n let maximumFractionDigits = 0;\n\n let className: string = null;\n if (ageGap > 1000) {\n className = \"giga\";\n } else if (ageGap > 100) {\n className = \"mega\";\n } else if (ageGap > 10) {\n className = \"large\";\n } else if (ageGap < 1) {\n className = \"small\";\n maximumFractionDigits = 2;\n } else {\n maximumFractionDigits = 1;\n }\n\n let val: ReactNode;\n if (axisType === ColumnAxisType.DEPTH || axisType === ColumnAxisType.HEIGHT) {\n const _txt = ageGap.toLocaleString(\"en-US\", { maximumFractionDigits });\n val = h(Value, { value: _txt, unit: \"m\" });\n } else {\n val = h(Duration, { value: ageGap, maximumFractionDigits });\n }\n\n let prefix: ReactNode = null;\n if (verbose) {\n prefix = h([\" \", h(\"span.prefix\", \" gap\")]);\n }\n\n return h(\"div.unconformity\", { style, className }, [\n h(\"div.unconformity-inner\", h(\"div.unconformity-text\", [val, prefix])),\n ]);\n}\n"],"names":["hyper","styles","levelCount"],"mappings":"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"}
@@ -0,0 +1,191 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const React = require("react");
4
+ const animatedAgeWindow = require("./animated-age-window.cjs");
5
+ const MIN_TIMESCALE_LEVEL = 1;
6
+ const MAX_TIMESCALE_LEVEL = 5;
7
+ const LEVEL_WINDOW = 3;
8
+ const DEFAULT_LEVEL = 3;
9
+ const SELECTED_INTERVAL_STYLE = { fontWeight: "bold" };
10
+ function useTimescaleZoom(options) {
11
+ const {
12
+ fullExtent,
13
+ enabled = true,
14
+ levelWindow = LEVEL_WINDOW,
15
+ defaultLevel = DEFAULT_LEVEL,
16
+ minLevel = MIN_TIMESCALE_LEVEL,
17
+ maxLevel = MAX_TIMESCALE_LEVEL,
18
+ duration,
19
+ selectedIntervalStyle = SELECTED_INTERVAL_STYLE
20
+ } = options;
21
+ const anim = animatedAgeWindow.useAnimatedAgeWindow({ fullExtent, duration });
22
+ const [intervals, setIntervals] = React.useState([]);
23
+ const [addedIntervals, setAddedIntervals] = React.useState([]);
24
+ const [selectedTimescaleID, setSelectedTimescaleID] = React.useState(
25
+ null
26
+ );
27
+ const selectedInterval = intervals[intervals.length - 1] ?? null;
28
+ const selectedIntervals = React.useMemo(() => {
29
+ if (selectedInterval == null) return addedIntervals;
30
+ return [selectedInterval, ...addedIntervals];
31
+ }, [selectedInterval, addedIntervals]);
32
+ const reset = React.useCallback(() => {
33
+ setIntervals([]);
34
+ setAddedIntervals([]);
35
+ setSelectedTimescaleID(null);
36
+ anim.reset();
37
+ }, [anim.reset]);
38
+ const extendToInterval = React.useCallback(
39
+ (interval) => {
40
+ let added = addedIntervals.filter((d) => d.oid !== interval.oid);
41
+ const wasSelected = added.length !== addedIntervals.length;
42
+ if (!wasSelected && interval.oid !== selectedInterval?.oid) {
43
+ added = [...addedIntervals, interval];
44
+ }
45
+ setAddedIntervals(added);
46
+ let selection = added;
47
+ if (selectedInterval != null) {
48
+ selection = [selectedInterval, ...added];
49
+ }
50
+ if (selection.length === 0) {
51
+ anim.reset();
52
+ return;
53
+ }
54
+ anim.zoomToWindow(intervalsWindow(selection));
55
+ },
56
+ [addedIntervals, selectedInterval, anim.zoomToWindow, anim.reset]
57
+ );
58
+ const zoomToInterval = React.useCallback(
59
+ (interval, timescaleID = null) => {
60
+ const containing = intervals.filter(
61
+ (d) => d.lvl < interval.lvl && d.eag >= interval.eag && d.lag <= interval.lag
62
+ );
63
+ setIntervals([...containing, interval]);
64
+ setAddedIntervals([]);
65
+ setSelectedTimescaleID(timescaleID);
66
+ anim.zoomToInterval(interval);
67
+ },
68
+ [intervals, anim.zoomToInterval]
69
+ );
70
+ const onClickTimescaleInterval = React.useCallback(
71
+ (event, data) => {
72
+ const interval = data?.interval;
73
+ if (interval == null || interval.lvl == null) return;
74
+ const timescaleID = data.timescaleID ?? null;
75
+ if (event?.shiftKey) {
76
+ extendToInterval(interval);
77
+ return;
78
+ }
79
+ if (interval.oid === selectedInterval?.oid && addedIntervals.length === 0) {
80
+ const next = intervals.slice(0, -1);
81
+ setIntervals(next);
82
+ setSelectedTimescaleID(null);
83
+ const parent = next[next.length - 1] ?? null;
84
+ if (parent == null) {
85
+ anim.reset();
86
+ } else {
87
+ anim.zoomToInterval(parent);
88
+ }
89
+ return;
90
+ }
91
+ zoomToInterval(interval, timescaleID);
92
+ },
93
+ [
94
+ intervals,
95
+ selectedInterval,
96
+ selectedTimescaleID,
97
+ addedIntervals,
98
+ extendToInterval,
99
+ zoomToInterval,
100
+ anim.reset
101
+ ]
102
+ );
103
+ const timescaleIntervalStyle = React.useCallback(
104
+ (interval) => {
105
+ if (selectedIntervals.some((d) => d.oid === interval.oid)) {
106
+ return selectedIntervalStyle;
107
+ }
108
+ return {};
109
+ },
110
+ [selectedIntervals, selectedIntervalStyle]
111
+ );
112
+ const levels = React.useMemo(() => {
113
+ let level = defaultLevel;
114
+ for (const interval of selectedIntervals) {
115
+ if (interval.lvl != null) level = Math.max(level, interval.lvl);
116
+ }
117
+ return levelsForSelected(level, { levelWindow, minLevel, maxLevel });
118
+ }, [selectedIntervals, defaultLevel, levelWindow, minLevel, maxLevel]);
119
+ if (!enabled) {
120
+ return {
121
+ ...disabledZoom,
122
+ selectedTimescaleID: null,
123
+ selectedIntervals: [],
124
+ timescaleLevels: levels,
125
+ reset,
126
+ zoomToInterval,
127
+ onClickTimescaleInterval,
128
+ timescaleIntervalStyle
129
+ };
130
+ }
131
+ const window = anim.window ?? fullExtent;
132
+ return {
133
+ enabled: true,
134
+ window,
135
+ intervals,
136
+ selectedInterval,
137
+ selectedTimescaleID,
138
+ selectedIntervals,
139
+ timescaleLevels: levels,
140
+ isFullExtent: anim.isFullExtent,
141
+ isAnimating: anim.isAnimating,
142
+ reset,
143
+ zoomToInterval,
144
+ onClickTimescaleInterval,
145
+ timescaleIntervalStyle,
146
+ columnProps: {
147
+ showTimescale: true,
148
+ timescaleLevels: levels,
149
+ timescaleIntervalStyle,
150
+ onClickTimescaleInterval,
151
+ t_age: window?.t_age,
152
+ b_age: window?.b_age,
153
+ isTransitioning: anim.isAnimating
154
+ }
155
+ };
156
+ }
157
+ const disabledZoom = {
158
+ enabled: false,
159
+ window: null,
160
+ intervals: [],
161
+ selectedInterval: null,
162
+ isFullExtent: true,
163
+ isAnimating: false,
164
+ columnProps: {}
165
+ };
166
+ function levelsForSelected(selectedLevel, options) {
167
+ const { levelWindow, minLevel, maxLevel } = options;
168
+ const span = Math.max(levelWindow - 1, 0);
169
+ const lo = Math.min(
170
+ Math.max(selectedLevel - 1, minLevel),
171
+ Math.max(maxLevel - span, minLevel)
172
+ );
173
+ return [lo, lo + span];
174
+ }
175
+ function intervalsWindow(intervals) {
176
+ return {
177
+ t_age: Math.min(...intervals.map((d) => d.lag)),
178
+ b_age: Math.max(...intervals.map((d) => d.eag))
179
+ };
180
+ }
181
+ function unitsAgeExtent(units) {
182
+ if (units == null || units.length === 0) return null;
183
+ return {
184
+ t_age: Math.min(...units.map((d) => d.t_age)),
185
+ b_age: Math.max(...units.map((d) => d.b_age))
186
+ };
187
+ }
188
+ exports.levelsForSelected = levelsForSelected;
189
+ exports.unitsAgeExtent = unitsAgeExtent;
190
+ exports.useTimescaleZoom = useTimescaleZoom;
191
+ //# sourceMappingURL=timescale-zoom.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timescale-zoom.cjs","sources":["../src/timescale-zoom.ts"],"sourcesContent":["/** Click-to-zoom navigation over geologic time.\n *\n * `useTimescaleZoom` turns clicks on a column's timescale into an animated\n * age window: clicking an interval zooms to it, clicking the interval you are\n * already in zooms back out a level, and the timescale shows a sliding window\n * of levels that follows the selection, so finer intervals come into reach as\n * you drill. The result spreads straight onto a `Column`.\n */\nimport { type CSSProperties, useCallback, useMemo, useState } from \"react\";\nimport type {\n Interval,\n IntervalStyleBuilder,\n TimescaleClickData,\n TimescaleClickHandler,\n} from \"@macrostrat/timescale\";\nimport { type AgeWindow, useAnimatedAgeWindow } from \"./animated-age-window\";\n\n/** Coarsest level worth showing; level 0 is \"all of geologic time\" */\nconst MIN_TIMESCALE_LEVEL = 1;\n/** Deepest level in the timescale (age/stage) */\nconst MAX_TIMESCALE_LEVEL = 5;\n/** How many levels are shown at once */\nconst LEVEL_WINDOW = 3;\n/** The level anchored on before an interval is picked. 3 (period) puts the\n * starting window at era–epoch, the levels a `Column` shows by default. */\nconst DEFAULT_LEVEL = 3;\n\nconst SELECTED_INTERVAL_STYLE: CSSProperties = { fontWeight: \"bold\" };\n\nexport interface UseTimescaleZoomOptions {\n /** The full data extent — the window zooming returns to. `null` until the\n * data (and hence the extent) is known. */\n fullExtent: AgeWindow | null;\n /** When false the hook reports no window and no column props, so a view can\n * offer zooming as an option without changing hook order (default `true`). */\n enabled?: boolean;\n /** How many timescale levels to show at once (default 3) */\n levelWindow?: number;\n /** The level to anchor the window on before an interval is picked\n * (default 3, period) */\n defaultLevel?: number;\n minLevel?: number;\n maxLevel?: number;\n /** Animation duration in ms */\n duration?: number;\n /** Style for the selected interval — the one whose click zooms out\n * (default bold) */\n selectedIntervalStyle?: CSSProperties;\n}\n\n/** Timescale and age-window props, ready to spread onto a `Column`. */\nexport interface TimescaleZoomColumnProps {\n showTimescale?: boolean;\n timescaleLevels?: [number, number];\n timescaleIntervalStyle?: IntervalStyleBuilder;\n onClickTimescaleInterval?: TimescaleClickHandler;\n t_age?: number;\n b_age?: number;\n isTransitioning?: boolean;\n}\n\nexport interface TimescaleZoom {\n enabled: boolean;\n /** The rendered age window — `null` when disabled or before the extent is\n * known */\n window: AgeWindow | null;\n /** The drill path: coarse to fine, the last entry being the selection */\n intervals: Interval[];\n selectedInterval: Interval | null;\n /** The timescale the selection was made in, when the click said so */\n selectedTimescaleID: number | null;\n /** Every interval in the selection: the one drilled to, plus any added by\n * shift-clicking. The window spans all of them. */\n selectedIntervals: Interval[];\n /** The levels the timescale should show, following the selection */\n timescaleLevels: [number, number];\n isFullExtent: boolean;\n isAnimating: boolean;\n /** Return to the full extent, clearing the drill path */\n reset(): void;\n zoomToInterval(interval: Interval): void;\n onClickTimescaleInterval: TimescaleClickHandler;\n timescaleIntervalStyle: (interval: Interval) => CSSProperties;\n columnProps: TimescaleZoomColumnProps;\n}\n\nexport function useTimescaleZoom(\n options: UseTimescaleZoomOptions,\n): TimescaleZoom {\n const {\n fullExtent,\n enabled = true,\n levelWindow = LEVEL_WINDOW,\n defaultLevel = DEFAULT_LEVEL,\n minLevel = MIN_TIMESCALE_LEVEL,\n maxLevel = MAX_TIMESCALE_LEVEL,\n duration,\n selectedIntervalStyle = SELECTED_INTERVAL_STYLE,\n } = options;\n\n const anim = useAnimatedAgeWindow({ fullExtent, duration });\n\n // The intervals drilled through; the last one is the current selection\n const [intervals, setIntervals] = useState<Interval[]>([]);\n // Intervals shift-clicked into the selection alongside it\n const [addedIntervals, setAddedIntervals] = useState<Interval[]>([]);\n // Which timescale the selection was made in. The same interval is drawn in\n // every timescale that contains it, and those copies are not the same\n // selection: clicking one in another column moves the selection there.\n const [selectedTimescaleID, setSelectedTimescaleID] = useState<number | null>(\n null,\n );\n const selectedInterval = intervals[intervals.length - 1] ?? null;\n\n const selectedIntervals = useMemo(() => {\n if (selectedInterval == null) return addedIntervals;\n return [selectedInterval, ...addedIntervals];\n }, [selectedInterval, addedIntervals]);\n\n const reset = useCallback(() => {\n setIntervals([]);\n setAddedIntervals([]);\n setSelectedTimescaleID(null);\n anim.reset();\n }, [anim.reset]);\n\n /** Take the interval clicked into (or out of) the selection, and span the\n * result — how you widen a window to the interval next door. */\n const extendToInterval = useCallback(\n (interval: Interval) => {\n let added = addedIntervals.filter((d) => d.oid !== interval.oid);\n const wasSelected = added.length !== addedIntervals.length;\n if (!wasSelected && interval.oid !== selectedInterval?.oid) {\n added = [...addedIntervals, interval];\n }\n setAddedIntervals(added);\n\n let selection = added;\n if (selectedInterval != null) {\n selection = [selectedInterval, ...added];\n }\n if (selection.length === 0) {\n anim.reset();\n return;\n }\n anim.zoomToWindow(intervalsWindow(selection));\n },\n [addedIntervals, selectedInterval, anim.zoomToWindow, anim.reset],\n );\n\n const zoomToInterval = useCallback(\n (interval: Interval, timescaleID: number | null = null) => {\n // Keep only the coarser intervals that actually contain this one, so\n // stepping sideways into a different parent (the last stage of the\n // Cambrian → the first of the Ordovician) doesn't strand the old one\n const containing = intervals.filter(\n (d) =>\n d.lvl < interval.lvl &&\n d.eag >= interval.eag &&\n d.lag <= interval.lag,\n );\n setIntervals([...containing, interval]);\n setAddedIntervals([]);\n setSelectedTimescaleID(timescaleID);\n anim.zoomToInterval(interval);\n },\n [intervals, anim.zoomToInterval],\n );\n\n const onClickTimescaleInterval = useCallback<TimescaleClickHandler>(\n (event: Event, data: TimescaleClickData & { timescaleID?: number }) => {\n const interval = data?.interval;\n if (interval == null || interval.lvl == null) return;\n\n const timescaleID = data.timescaleID ?? null;\n // A click in a different timescale is a different selection, even on the\n // same interval: it moves to that column rather than zooming back out\n const sameTimescale =\n timescaleID == null ||\n selectedTimescaleID == null ||\n timescaleID === selectedTimescaleID;\n\n // Shift-click widens the window instead of moving it, so a selection can\n // grow into the interval next door\n if ((event as MouseEvent)?.shiftKey) {\n extendToInterval(interval);\n return;\n }\n\n // Clicking the interval you are in is the way back out: pop a level, or\n // return to the full extent past the root\n if (interval.oid === selectedInterval?.oid && addedIntervals.length === 0) {\n const next = intervals.slice(0, -1);\n setIntervals(next);\n setSelectedTimescaleID(null);\n const parent = next[next.length - 1] ?? null;\n if (parent == null) {\n anim.reset();\n } else {\n anim.zoomToInterval(parent);\n }\n return;\n }\n\n // Every other click navigates *to* the interval clicked, whatever its\n // rank: a finer one drills in, a neighbor moves along the timescale, a\n // coarser one zooms out to it\n zoomToInterval(interval, timescaleID);\n },\n [\n intervals,\n selectedInterval,\n selectedTimescaleID,\n addedIntervals,\n extendToInterval,\n zoomToInterval,\n anim.reset,\n ],\n );\n\n const timescaleIntervalStyle = useCallback(\n (interval: Interval): CSSProperties => {\n if (selectedIntervals.some((d) => d.oid === interval.oid)) {\n return selectedIntervalStyle;\n }\n return {};\n },\n [selectedIntervals, selectedIntervalStyle],\n );\n\n const levels = useMemo(() => {\n let level = defaultLevel;\n for (const interval of selectedIntervals) {\n if (interval.lvl != null) level = Math.max(level, interval.lvl);\n }\n return levelsForSelected(level, { levelWindow, minLevel, maxLevel });\n }, [selectedIntervals, defaultLevel, levelWindow, minLevel, maxLevel]);\n\n if (!enabled) {\n return {\n ...disabledZoom,\n selectedTimescaleID: null,\n selectedIntervals: [],\n timescaleLevels: levels,\n reset,\n zoomToInterval,\n onClickTimescaleInterval,\n timescaleIntervalStyle,\n };\n }\n\n const window = anim.window ?? fullExtent;\n\n return {\n enabled: true,\n window,\n intervals,\n selectedInterval,\n selectedTimescaleID,\n selectedIntervals,\n timescaleLevels: levels,\n isFullExtent: anim.isFullExtent,\n isAnimating: anim.isAnimating,\n reset,\n zoomToInterval,\n onClickTimescaleInterval,\n timescaleIntervalStyle,\n columnProps: {\n showTimescale: true,\n timescaleLevels: levels,\n timescaleIntervalStyle,\n onClickTimescaleInterval,\n t_age: window?.t_age,\n b_age: window?.b_age,\n isTransitioning: anim.isAnimating,\n },\n };\n}\n\n/** What the hook reports when zooming is switched off: no window, and no\n * props to change how the column renders. */\nconst disabledZoom: Pick<\n TimescaleZoom,\n | \"enabled\"\n | \"window\"\n | \"intervals\"\n | \"selectedInterval\"\n | \"isFullExtent\"\n | \"isAnimating\"\n | \"columnProps\"\n> = {\n enabled: false,\n window: null,\n intervals: [],\n selectedInterval: null,\n isFullExtent: true,\n isAnimating: false,\n columnProps: {},\n};\n\ninterface LevelWindowOptions {\n levelWindow: number;\n minLevel: number;\n maxLevel: number;\n}\n\n/** A fixed window of timescale levels that slides with the selected level —\n * one coarser for context, the rest finer to drill into. Clamped so the\n * useless level 0 is never shown. */\nexport function levelsForSelected(\n selectedLevel: number,\n options: LevelWindowOptions,\n): [number, number] {\n const { levelWindow, minLevel, maxLevel } = options;\n const span = Math.max(levelWindow - 1, 0);\n const lo = Math.min(\n Math.max(selectedLevel - 1, minLevel),\n Math.max(maxLevel - span, minLevel),\n );\n return [lo, lo + span];\n}\n\n/** The window spanning a set of intervals. */\nfunction intervalsWindow(intervals: Interval[]): AgeWindow {\n return {\n t_age: Math.min(...intervals.map((d) => d.lag)),\n b_age: Math.max(...intervals.map((d) => d.eag)),\n };\n}\n\n/** The age extent of a set of units, in the shape the age-window props take.\n * `null` for an empty or absent set. */\nexport function unitsAgeExtent(\n units: { t_age: number; b_age: number }[] | null | undefined,\n): AgeWindow | null {\n if (units == null || units.length === 0) return null;\n return {\n t_age: Math.min(...units.map((d) => d.t_age)),\n b_age: Math.max(...units.map((d) => d.b_age)),\n };\n}\n"],"names":["useAnimatedAgeWindow","useState","useMemo","useCallback"],"mappings":"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"}
@@ -0,0 +1,74 @@
1
+ import { CSSProperties } from 'react';
2
+ import { Interval, IntervalStyleBuilder, TimescaleClickHandler } from '@macrostrat/timescale';
3
+ import { AgeWindow } from './animated-age-window';
4
+ export interface UseTimescaleZoomOptions {
5
+ /** The full data extent — the window zooming returns to. `null` until the
6
+ * data (and hence the extent) is known. */
7
+ fullExtent: AgeWindow | null;
8
+ /** When false the hook reports no window and no column props, so a view can
9
+ * offer zooming as an option without changing hook order (default `true`). */
10
+ enabled?: boolean;
11
+ /** How many timescale levels to show at once (default 3) */
12
+ levelWindow?: number;
13
+ /** The level to anchor the window on before an interval is picked
14
+ * (default 3, period) */
15
+ defaultLevel?: number;
16
+ minLevel?: number;
17
+ maxLevel?: number;
18
+ /** Animation duration in ms */
19
+ duration?: number;
20
+ /** Style for the selected interval — the one whose click zooms out
21
+ * (default bold) */
22
+ selectedIntervalStyle?: CSSProperties;
23
+ }
24
+ /** Timescale and age-window props, ready to spread onto a `Column`. */
25
+ export interface TimescaleZoomColumnProps {
26
+ showTimescale?: boolean;
27
+ timescaleLevels?: [number, number];
28
+ timescaleIntervalStyle?: IntervalStyleBuilder;
29
+ onClickTimescaleInterval?: TimescaleClickHandler;
30
+ t_age?: number;
31
+ b_age?: number;
32
+ isTransitioning?: boolean;
33
+ }
34
+ export interface TimescaleZoom {
35
+ enabled: boolean;
36
+ /** The rendered age window — `null` when disabled or before the extent is
37
+ * known */
38
+ window: AgeWindow | null;
39
+ /** The drill path: coarse to fine, the last entry being the selection */
40
+ intervals: Interval[];
41
+ selectedInterval: Interval | null;
42
+ /** The timescale the selection was made in, when the click said so */
43
+ selectedTimescaleID: number | null;
44
+ /** Every interval in the selection: the one drilled to, plus any added by
45
+ * shift-clicking. The window spans all of them. */
46
+ selectedIntervals: Interval[];
47
+ /** The levels the timescale should show, following the selection */
48
+ timescaleLevels: [number, number];
49
+ isFullExtent: boolean;
50
+ isAnimating: boolean;
51
+ /** Return to the full extent, clearing the drill path */
52
+ reset(): void;
53
+ zoomToInterval(interval: Interval): void;
54
+ onClickTimescaleInterval: TimescaleClickHandler;
55
+ timescaleIntervalStyle: (interval: Interval) => CSSProperties;
56
+ columnProps: TimescaleZoomColumnProps;
57
+ }
58
+ export declare function useTimescaleZoom(options: UseTimescaleZoomOptions): TimescaleZoom;
59
+ interface LevelWindowOptions {
60
+ levelWindow: number;
61
+ minLevel: number;
62
+ maxLevel: number;
63
+ }
64
+ /** A fixed window of timescale levels that slides with the selected level —
65
+ * one coarser for context, the rest finer to drill into. Clamped so the
66
+ * useless level 0 is never shown. */
67
+ export declare function levelsForSelected(selectedLevel: number, options: LevelWindowOptions): [number, number];
68
+ /** The age extent of a set of units, in the shape the age-window props take.
69
+ * `null` for an empty or absent set. */
70
+ export declare function unitsAgeExtent(units: {
71
+ t_age: number;
72
+ b_age: number;
73
+ }[] | null | undefined): AgeWindow | null;
74
+ export {};
@@ -0,0 +1,191 @@
1
+ import { useState, useMemo, useCallback } from "react";
2
+ import { useAnimatedAgeWindow } from "./animated-age-window.js";
3
+ const MIN_TIMESCALE_LEVEL = 1;
4
+ const MAX_TIMESCALE_LEVEL = 5;
5
+ const LEVEL_WINDOW = 3;
6
+ const DEFAULT_LEVEL = 3;
7
+ const SELECTED_INTERVAL_STYLE = { fontWeight: "bold" };
8
+ function useTimescaleZoom(options) {
9
+ const {
10
+ fullExtent,
11
+ enabled = true,
12
+ levelWindow = LEVEL_WINDOW,
13
+ defaultLevel = DEFAULT_LEVEL,
14
+ minLevel = MIN_TIMESCALE_LEVEL,
15
+ maxLevel = MAX_TIMESCALE_LEVEL,
16
+ duration,
17
+ selectedIntervalStyle = SELECTED_INTERVAL_STYLE
18
+ } = options;
19
+ const anim = useAnimatedAgeWindow({ fullExtent, duration });
20
+ const [intervals, setIntervals] = useState([]);
21
+ const [addedIntervals, setAddedIntervals] = useState([]);
22
+ const [selectedTimescaleID, setSelectedTimescaleID] = useState(
23
+ null
24
+ );
25
+ const selectedInterval = intervals[intervals.length - 1] ?? null;
26
+ const selectedIntervals = useMemo(() => {
27
+ if (selectedInterval == null) return addedIntervals;
28
+ return [selectedInterval, ...addedIntervals];
29
+ }, [selectedInterval, addedIntervals]);
30
+ const reset = useCallback(() => {
31
+ setIntervals([]);
32
+ setAddedIntervals([]);
33
+ setSelectedTimescaleID(null);
34
+ anim.reset();
35
+ }, [anim.reset]);
36
+ const extendToInterval = useCallback(
37
+ (interval) => {
38
+ let added = addedIntervals.filter((d) => d.oid !== interval.oid);
39
+ const wasSelected = added.length !== addedIntervals.length;
40
+ if (!wasSelected && interval.oid !== selectedInterval?.oid) {
41
+ added = [...addedIntervals, interval];
42
+ }
43
+ setAddedIntervals(added);
44
+ let selection = added;
45
+ if (selectedInterval != null) {
46
+ selection = [selectedInterval, ...added];
47
+ }
48
+ if (selection.length === 0) {
49
+ anim.reset();
50
+ return;
51
+ }
52
+ anim.zoomToWindow(intervalsWindow(selection));
53
+ },
54
+ [addedIntervals, selectedInterval, anim.zoomToWindow, anim.reset]
55
+ );
56
+ const zoomToInterval = useCallback(
57
+ (interval, timescaleID = null) => {
58
+ const containing = intervals.filter(
59
+ (d) => d.lvl < interval.lvl && d.eag >= interval.eag && d.lag <= interval.lag
60
+ );
61
+ setIntervals([...containing, interval]);
62
+ setAddedIntervals([]);
63
+ setSelectedTimescaleID(timescaleID);
64
+ anim.zoomToInterval(interval);
65
+ },
66
+ [intervals, anim.zoomToInterval]
67
+ );
68
+ const onClickTimescaleInterval = useCallback(
69
+ (event, data) => {
70
+ const interval = data?.interval;
71
+ if (interval == null || interval.lvl == null) return;
72
+ const timescaleID = data.timescaleID ?? null;
73
+ if (event?.shiftKey) {
74
+ extendToInterval(interval);
75
+ return;
76
+ }
77
+ if (interval.oid === selectedInterval?.oid && addedIntervals.length === 0) {
78
+ const next = intervals.slice(0, -1);
79
+ setIntervals(next);
80
+ setSelectedTimescaleID(null);
81
+ const parent = next[next.length - 1] ?? null;
82
+ if (parent == null) {
83
+ anim.reset();
84
+ } else {
85
+ anim.zoomToInterval(parent);
86
+ }
87
+ return;
88
+ }
89
+ zoomToInterval(interval, timescaleID);
90
+ },
91
+ [
92
+ intervals,
93
+ selectedInterval,
94
+ selectedTimescaleID,
95
+ addedIntervals,
96
+ extendToInterval,
97
+ zoomToInterval,
98
+ anim.reset
99
+ ]
100
+ );
101
+ const timescaleIntervalStyle = useCallback(
102
+ (interval) => {
103
+ if (selectedIntervals.some((d) => d.oid === interval.oid)) {
104
+ return selectedIntervalStyle;
105
+ }
106
+ return {};
107
+ },
108
+ [selectedIntervals, selectedIntervalStyle]
109
+ );
110
+ const levels = useMemo(() => {
111
+ let level = defaultLevel;
112
+ for (const interval of selectedIntervals) {
113
+ if (interval.lvl != null) level = Math.max(level, interval.lvl);
114
+ }
115
+ return levelsForSelected(level, { levelWindow, minLevel, maxLevel });
116
+ }, [selectedIntervals, defaultLevel, levelWindow, minLevel, maxLevel]);
117
+ if (!enabled) {
118
+ return {
119
+ ...disabledZoom,
120
+ selectedTimescaleID: null,
121
+ selectedIntervals: [],
122
+ timescaleLevels: levels,
123
+ reset,
124
+ zoomToInterval,
125
+ onClickTimescaleInterval,
126
+ timescaleIntervalStyle
127
+ };
128
+ }
129
+ const window = anim.window ?? fullExtent;
130
+ return {
131
+ enabled: true,
132
+ window,
133
+ intervals,
134
+ selectedInterval,
135
+ selectedTimescaleID,
136
+ selectedIntervals,
137
+ timescaleLevels: levels,
138
+ isFullExtent: anim.isFullExtent,
139
+ isAnimating: anim.isAnimating,
140
+ reset,
141
+ zoomToInterval,
142
+ onClickTimescaleInterval,
143
+ timescaleIntervalStyle,
144
+ columnProps: {
145
+ showTimescale: true,
146
+ timescaleLevels: levels,
147
+ timescaleIntervalStyle,
148
+ onClickTimescaleInterval,
149
+ t_age: window?.t_age,
150
+ b_age: window?.b_age,
151
+ isTransitioning: anim.isAnimating
152
+ }
153
+ };
154
+ }
155
+ const disabledZoom = {
156
+ enabled: false,
157
+ window: null,
158
+ intervals: [],
159
+ selectedInterval: null,
160
+ isFullExtent: true,
161
+ isAnimating: false,
162
+ columnProps: {}
163
+ };
164
+ function levelsForSelected(selectedLevel, options) {
165
+ const { levelWindow, minLevel, maxLevel } = options;
166
+ const span = Math.max(levelWindow - 1, 0);
167
+ const lo = Math.min(
168
+ Math.max(selectedLevel - 1, minLevel),
169
+ Math.max(maxLevel - span, minLevel)
170
+ );
171
+ return [lo, lo + span];
172
+ }
173
+ function intervalsWindow(intervals) {
174
+ return {
175
+ t_age: Math.min(...intervals.map((d) => d.lag)),
176
+ b_age: Math.max(...intervals.map((d) => d.eag))
177
+ };
178
+ }
179
+ function unitsAgeExtent(units) {
180
+ if (units == null || units.length === 0) return null;
181
+ return {
182
+ t_age: Math.min(...units.map((d) => d.t_age)),
183
+ b_age: Math.max(...units.map((d) => d.b_age))
184
+ };
185
+ }
186
+ export {
187
+ levelsForSelected,
188
+ unitsAgeExtent,
189
+ useTimescaleZoom
190
+ };
191
+ //# sourceMappingURL=timescale-zoom.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"timescale-zoom.js","sources":["../src/timescale-zoom.ts"],"sourcesContent":["/** Click-to-zoom navigation over geologic time.\n *\n * `useTimescaleZoom` turns clicks on a column's timescale into an animated\n * age window: clicking an interval zooms to it, clicking the interval you are\n * already in zooms back out a level, and the timescale shows a sliding window\n * of levels that follows the selection, so finer intervals come into reach as\n * you drill. The result spreads straight onto a `Column`.\n */\nimport { type CSSProperties, useCallback, useMemo, useState } from \"react\";\nimport type {\n Interval,\n IntervalStyleBuilder,\n TimescaleClickData,\n TimescaleClickHandler,\n} from \"@macrostrat/timescale\";\nimport { type AgeWindow, useAnimatedAgeWindow } from \"./animated-age-window\";\n\n/** Coarsest level worth showing; level 0 is \"all of geologic time\" */\nconst MIN_TIMESCALE_LEVEL = 1;\n/** Deepest level in the timescale (age/stage) */\nconst MAX_TIMESCALE_LEVEL = 5;\n/** How many levels are shown at once */\nconst LEVEL_WINDOW = 3;\n/** The level anchored on before an interval is picked. 3 (period) puts the\n * starting window at era–epoch, the levels a `Column` shows by default. */\nconst DEFAULT_LEVEL = 3;\n\nconst SELECTED_INTERVAL_STYLE: CSSProperties = { fontWeight: \"bold\" };\n\nexport interface UseTimescaleZoomOptions {\n /** The full data extent — the window zooming returns to. `null` until the\n * data (and hence the extent) is known. */\n fullExtent: AgeWindow | null;\n /** When false the hook reports no window and no column props, so a view can\n * offer zooming as an option without changing hook order (default `true`). */\n enabled?: boolean;\n /** How many timescale levels to show at once (default 3) */\n levelWindow?: number;\n /** The level to anchor the window on before an interval is picked\n * (default 3, period) */\n defaultLevel?: number;\n minLevel?: number;\n maxLevel?: number;\n /** Animation duration in ms */\n duration?: number;\n /** Style for the selected interval — the one whose click zooms out\n * (default bold) */\n selectedIntervalStyle?: CSSProperties;\n}\n\n/** Timescale and age-window props, ready to spread onto a `Column`. */\nexport interface TimescaleZoomColumnProps {\n showTimescale?: boolean;\n timescaleLevels?: [number, number];\n timescaleIntervalStyle?: IntervalStyleBuilder;\n onClickTimescaleInterval?: TimescaleClickHandler;\n t_age?: number;\n b_age?: number;\n isTransitioning?: boolean;\n}\n\nexport interface TimescaleZoom {\n enabled: boolean;\n /** The rendered age window — `null` when disabled or before the extent is\n * known */\n window: AgeWindow | null;\n /** The drill path: coarse to fine, the last entry being the selection */\n intervals: Interval[];\n selectedInterval: Interval | null;\n /** The timescale the selection was made in, when the click said so */\n selectedTimescaleID: number | null;\n /** Every interval in the selection: the one drilled to, plus any added by\n * shift-clicking. The window spans all of them. */\n selectedIntervals: Interval[];\n /** The levels the timescale should show, following the selection */\n timescaleLevels: [number, number];\n isFullExtent: boolean;\n isAnimating: boolean;\n /** Return to the full extent, clearing the drill path */\n reset(): void;\n zoomToInterval(interval: Interval): void;\n onClickTimescaleInterval: TimescaleClickHandler;\n timescaleIntervalStyle: (interval: Interval) => CSSProperties;\n columnProps: TimescaleZoomColumnProps;\n}\n\nexport function useTimescaleZoom(\n options: UseTimescaleZoomOptions,\n): TimescaleZoom {\n const {\n fullExtent,\n enabled = true,\n levelWindow = LEVEL_WINDOW,\n defaultLevel = DEFAULT_LEVEL,\n minLevel = MIN_TIMESCALE_LEVEL,\n maxLevel = MAX_TIMESCALE_LEVEL,\n duration,\n selectedIntervalStyle = SELECTED_INTERVAL_STYLE,\n } = options;\n\n const anim = useAnimatedAgeWindow({ fullExtent, duration });\n\n // The intervals drilled through; the last one is the current selection\n const [intervals, setIntervals] = useState<Interval[]>([]);\n // Intervals shift-clicked into the selection alongside it\n const [addedIntervals, setAddedIntervals] = useState<Interval[]>([]);\n // Which timescale the selection was made in. The same interval is drawn in\n // every timescale that contains it, and those copies are not the same\n // selection: clicking one in another column moves the selection there.\n const [selectedTimescaleID, setSelectedTimescaleID] = useState<number | null>(\n null,\n );\n const selectedInterval = intervals[intervals.length - 1] ?? null;\n\n const selectedIntervals = useMemo(() => {\n if (selectedInterval == null) return addedIntervals;\n return [selectedInterval, ...addedIntervals];\n }, [selectedInterval, addedIntervals]);\n\n const reset = useCallback(() => {\n setIntervals([]);\n setAddedIntervals([]);\n setSelectedTimescaleID(null);\n anim.reset();\n }, [anim.reset]);\n\n /** Take the interval clicked into (or out of) the selection, and span the\n * result — how you widen a window to the interval next door. */\n const extendToInterval = useCallback(\n (interval: Interval) => {\n let added = addedIntervals.filter((d) => d.oid !== interval.oid);\n const wasSelected = added.length !== addedIntervals.length;\n if (!wasSelected && interval.oid !== selectedInterval?.oid) {\n added = [...addedIntervals, interval];\n }\n setAddedIntervals(added);\n\n let selection = added;\n if (selectedInterval != null) {\n selection = [selectedInterval, ...added];\n }\n if (selection.length === 0) {\n anim.reset();\n return;\n }\n anim.zoomToWindow(intervalsWindow(selection));\n },\n [addedIntervals, selectedInterval, anim.zoomToWindow, anim.reset],\n );\n\n const zoomToInterval = useCallback(\n (interval: Interval, timescaleID: number | null = null) => {\n // Keep only the coarser intervals that actually contain this one, so\n // stepping sideways into a different parent (the last stage of the\n // Cambrian → the first of the Ordovician) doesn't strand the old one\n const containing = intervals.filter(\n (d) =>\n d.lvl < interval.lvl &&\n d.eag >= interval.eag &&\n d.lag <= interval.lag,\n );\n setIntervals([...containing, interval]);\n setAddedIntervals([]);\n setSelectedTimescaleID(timescaleID);\n anim.zoomToInterval(interval);\n },\n [intervals, anim.zoomToInterval],\n );\n\n const onClickTimescaleInterval = useCallback<TimescaleClickHandler>(\n (event: Event, data: TimescaleClickData & { timescaleID?: number }) => {\n const interval = data?.interval;\n if (interval == null || interval.lvl == null) return;\n\n const timescaleID = data.timescaleID ?? null;\n // A click in a different timescale is a different selection, even on the\n // same interval: it moves to that column rather than zooming back out\n const sameTimescale =\n timescaleID == null ||\n selectedTimescaleID == null ||\n timescaleID === selectedTimescaleID;\n\n // Shift-click widens the window instead of moving it, so a selection can\n // grow into the interval next door\n if ((event as MouseEvent)?.shiftKey) {\n extendToInterval(interval);\n return;\n }\n\n // Clicking the interval you are in is the way back out: pop a level, or\n // return to the full extent past the root\n if (interval.oid === selectedInterval?.oid && addedIntervals.length === 0) {\n const next = intervals.slice(0, -1);\n setIntervals(next);\n setSelectedTimescaleID(null);\n const parent = next[next.length - 1] ?? null;\n if (parent == null) {\n anim.reset();\n } else {\n anim.zoomToInterval(parent);\n }\n return;\n }\n\n // Every other click navigates *to* the interval clicked, whatever its\n // rank: a finer one drills in, a neighbor moves along the timescale, a\n // coarser one zooms out to it\n zoomToInterval(interval, timescaleID);\n },\n [\n intervals,\n selectedInterval,\n selectedTimescaleID,\n addedIntervals,\n extendToInterval,\n zoomToInterval,\n anim.reset,\n ],\n );\n\n const timescaleIntervalStyle = useCallback(\n (interval: Interval): CSSProperties => {\n if (selectedIntervals.some((d) => d.oid === interval.oid)) {\n return selectedIntervalStyle;\n }\n return {};\n },\n [selectedIntervals, selectedIntervalStyle],\n );\n\n const levels = useMemo(() => {\n let level = defaultLevel;\n for (const interval of selectedIntervals) {\n if (interval.lvl != null) level = Math.max(level, interval.lvl);\n }\n return levelsForSelected(level, { levelWindow, minLevel, maxLevel });\n }, [selectedIntervals, defaultLevel, levelWindow, minLevel, maxLevel]);\n\n if (!enabled) {\n return {\n ...disabledZoom,\n selectedTimescaleID: null,\n selectedIntervals: [],\n timescaleLevels: levels,\n reset,\n zoomToInterval,\n onClickTimescaleInterval,\n timescaleIntervalStyle,\n };\n }\n\n const window = anim.window ?? fullExtent;\n\n return {\n enabled: true,\n window,\n intervals,\n selectedInterval,\n selectedTimescaleID,\n selectedIntervals,\n timescaleLevels: levels,\n isFullExtent: anim.isFullExtent,\n isAnimating: anim.isAnimating,\n reset,\n zoomToInterval,\n onClickTimescaleInterval,\n timescaleIntervalStyle,\n columnProps: {\n showTimescale: true,\n timescaleLevels: levels,\n timescaleIntervalStyle,\n onClickTimescaleInterval,\n t_age: window?.t_age,\n b_age: window?.b_age,\n isTransitioning: anim.isAnimating,\n },\n };\n}\n\n/** What the hook reports when zooming is switched off: no window, and no\n * props to change how the column renders. */\nconst disabledZoom: Pick<\n TimescaleZoom,\n | \"enabled\"\n | \"window\"\n | \"intervals\"\n | \"selectedInterval\"\n | \"isFullExtent\"\n | \"isAnimating\"\n | \"columnProps\"\n> = {\n enabled: false,\n window: null,\n intervals: [],\n selectedInterval: null,\n isFullExtent: true,\n isAnimating: false,\n columnProps: {},\n};\n\ninterface LevelWindowOptions {\n levelWindow: number;\n minLevel: number;\n maxLevel: number;\n}\n\n/** A fixed window of timescale levels that slides with the selected level —\n * one coarser for context, the rest finer to drill into. Clamped so the\n * useless level 0 is never shown. */\nexport function levelsForSelected(\n selectedLevel: number,\n options: LevelWindowOptions,\n): [number, number] {\n const { levelWindow, minLevel, maxLevel } = options;\n const span = Math.max(levelWindow - 1, 0);\n const lo = Math.min(\n Math.max(selectedLevel - 1, minLevel),\n Math.max(maxLevel - span, minLevel),\n );\n return [lo, lo + span];\n}\n\n/** The window spanning a set of intervals. */\nfunction intervalsWindow(intervals: Interval[]): AgeWindow {\n return {\n t_age: Math.min(...intervals.map((d) => d.lag)),\n b_age: Math.max(...intervals.map((d) => d.eag)),\n };\n}\n\n/** The age extent of a set of units, in the shape the age-window props take.\n * `null` for an empty or absent set. */\nexport function unitsAgeExtent(\n units: { t_age: number; b_age: number }[] | null | undefined,\n): AgeWindow | null {\n if (units == null || units.length === 0) return null;\n return {\n t_age: Math.min(...units.map((d) => d.t_age)),\n b_age: Math.max(...units.map((d) => d.b_age)),\n };\n}\n"],"names":[],"mappings":"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"}