@macrostrat/column-views 2.3.2 → 2.4.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.
Files changed (185) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/esm/{column-views.ce3a3ac8.js → column-views.0f910df6.js} +2 -2
  3. package/dist/esm/{column-views.ce3a3ac8.js.map → column-views.0f910df6.js.map} +1 -1
  4. package/dist/esm/{column-views.4e6eead3.js → column-views.1cd57b96.js} +2 -2
  5. package/dist/esm/{column-views.4e6eead3.js.map → column-views.1cd57b96.js.map} +1 -1
  6. package/dist/esm/{column-views.ee9d71ba.js → column-views.1d356b7e.js} +2 -2
  7. package/dist/esm/{column-views.ee9d71ba.js.map → column-views.1d356b7e.js.map} +1 -1
  8. package/dist/esm/{column-views.3d3e00be.js → column-views.1ef0f7e8.js} +2 -2
  9. package/dist/esm/{column-views.3d3e00be.js.map → column-views.1ef0f7e8.js.map} +1 -1
  10. package/dist/esm/{column-views.ad2fe46c.js → column-views.1f954206.js} +2 -2
  11. package/dist/esm/{column-views.ad2fe46c.js.map → column-views.1f954206.js.map} +1 -1
  12. package/dist/esm/{column-views.24060b85.js → column-views.25713aad.js} +4 -4
  13. package/dist/esm/{column-views.24060b85.js.map → column-views.25713aad.js.map} +1 -1
  14. package/dist/esm/{column-views.9ffc089b.js → column-views.28cab78c.js} +3 -3
  15. package/dist/esm/{column-views.9ffc089b.js.map → column-views.28cab78c.js.map} +1 -1
  16. package/dist/esm/column-views.2e517e39.js +126 -0
  17. package/dist/esm/column-views.2e517e39.js.map +1 -0
  18. package/dist/esm/{column-views.18aac6a5.js → column-views.3067c5e3.js} +3 -3
  19. package/dist/esm/{column-views.18aac6a5.js.map → column-views.3067c5e3.js.map} +1 -1
  20. package/dist/esm/{column-views.6f96d1c3.js → column-views.31152763.js} +20 -17
  21. package/dist/esm/column-views.31152763.js.map +1 -0
  22. package/dist/esm/{column-views.04636815.js → column-views.33daabd5.js} +5 -5
  23. package/dist/esm/{column-views.04636815.js.map → column-views.33daabd5.js.map} +1 -1
  24. package/dist/esm/{column-views.e075af15.js → column-views.3c1cde51.js} +2 -2
  25. package/dist/esm/{column-views.e075af15.js.map → column-views.3c1cde51.js.map} +1 -1
  26. package/dist/esm/{column-views.0da4503a.js → column-views.3c9f5ba9.js} +2 -2
  27. package/dist/esm/{column-views.0da4503a.js.map → column-views.3c9f5ba9.js.map} +1 -1
  28. package/dist/esm/{column-views.c149f7a5.js → column-views.3d33ef0f.js} +3 -3
  29. package/dist/esm/{column-views.c149f7a5.js.map → column-views.3d33ef0f.js.map} +1 -1
  30. package/dist/esm/{column-views.8d972657.js → column-views.4099a246.js} +3 -3
  31. package/dist/esm/{column-views.8d972657.js.map → column-views.4099a246.js.map} +1 -1
  32. package/dist/esm/{column-views.b9265595.js → column-views.45acb682.js} +6 -6
  33. package/dist/esm/column-views.45acb682.js.map +1 -0
  34. package/dist/esm/{column-views.26e0e0b6.js → column-views.4e5746da.js} +2 -2
  35. package/dist/esm/{column-views.26e0e0b6.js.map → column-views.4e5746da.js.map} +1 -1
  36. package/dist/esm/{column-views.f7cdf6be.js → column-views.55bce35c.js} +3 -3
  37. package/dist/esm/{column-views.f7cdf6be.js.map → column-views.55bce35c.js.map} +1 -1
  38. package/dist/esm/{column-views.029ea8f1.js → column-views.631f55d4.js} +3 -3
  39. package/dist/esm/{column-views.029ea8f1.js.map → column-views.631f55d4.js.map} +1 -1
  40. package/dist/esm/column-views.67804ce5.js +68 -0
  41. package/dist/esm/column-views.67804ce5.js.map +1 -0
  42. package/dist/esm/column-views.8168c37d.js +113 -0
  43. package/dist/esm/column-views.8168c37d.js.map +1 -0
  44. package/dist/esm/{column-views.b977da35.js → column-views.848349c0.js} +5 -5
  45. package/dist/esm/{column-views.b977da35.js.map → column-views.848349c0.js.map} +1 -1
  46. package/dist/esm/{column-views.73fc96cb.js → column-views.86b879ba.js} +5 -5
  47. package/dist/esm/{column-views.73fc96cb.js.map → column-views.86b879ba.js.map} +1 -1
  48. package/dist/esm/{column-views.d6c0b7bc.js → column-views.8c8f3fe0.js} +2 -2
  49. package/dist/esm/{column-views.d6c0b7bc.js.map → column-views.8c8f3fe0.js.map} +1 -1
  50. package/dist/esm/{column-views.3f1cdc18.js → column-views.9b210fb2.js} +2 -2
  51. package/dist/esm/{column-views.3f1cdc18.js.map → column-views.9b210fb2.js.map} +1 -1
  52. package/dist/esm/{column-views.4685ea28.js → column-views.9d0d78aa.js} +2 -2
  53. package/dist/esm/{column-views.4685ea28.js.map → column-views.9d0d78aa.js.map} +1 -1
  54. package/dist/esm/{column-views.e3f0cd7b.js → column-views.af89f2b1.js} +23 -33
  55. package/dist/esm/column-views.af89f2b1.js.map +1 -0
  56. package/dist/esm/{column-views.75b83110.js → column-views.b3398cca.js} +5 -5
  57. package/dist/esm/{column-views.75b83110.js.map → column-views.b3398cca.js.map} +1 -1
  58. package/dist/esm/{column-views.2f979aee.js → column-views.b7b0be88.js} +3 -3
  59. package/dist/esm/{column-views.2f979aee.js.map → column-views.b7b0be88.js.map} +1 -1
  60. package/dist/esm/{column-views.d1b49f5c.js → column-views.b8933ea2.js} +2 -2
  61. package/dist/esm/{column-views.d1b49f5c.js.map → column-views.b8933ea2.js.map} +1 -1
  62. package/dist/esm/{column-views.0a010643.js → column-views.b9c4d94c.js} +4 -4
  63. package/dist/esm/{column-views.0a010643.js.map → column-views.b9c4d94c.js.map} +1 -1
  64. package/dist/esm/{column-views.5fff0716.js → column-views.d3a4596e.js} +3 -3
  65. package/dist/esm/{column-views.5fff0716.js.map → column-views.d3a4596e.js.map} +1 -1
  66. package/dist/esm/{column-views.d5fff9b8.js → column-views.dda76243.js} +3 -3
  67. package/dist/esm/{column-views.d5fff9b8.js.map → column-views.dda76243.js.map} +1 -1
  68. package/dist/esm/{column-views.9d51a5ab.js → column-views.df872a2c.js} +2 -2
  69. package/dist/esm/column-views.df872a2c.js.map +1 -0
  70. package/dist/esm/{column-views.488f24b6.js → column-views.e21f4e50.js} +2 -2
  71. package/dist/esm/{column-views.488f24b6.js.map → column-views.e21f4e50.js.map} +1 -1
  72. package/dist/esm/{column-views.a665cbb2.js → column-views.f046b170.js} +5 -5
  73. package/dist/esm/{column-views.a665cbb2.js.map → column-views.f046b170.js.map} +1 -1
  74. package/dist/esm/{column-views.82a5112b.js → column-views.fc4d28d8.js} +4 -4
  75. package/dist/esm/{column-views.82a5112b.js.map → column-views.fc4d28d8.js.map} +1 -1
  76. package/dist/esm/{column-views.faa7e52e.js → column-views.fdb3799c.js} +3 -3
  77. package/dist/esm/{column-views.faa7e52e.js.map → column-views.fdb3799c.js.map} +1 -1
  78. package/dist/esm/index.d.ts +73 -57
  79. package/dist/esm/index.d.ts.map +1 -1
  80. package/dist/esm/index.js +11 -11
  81. package/dist/node/{column-views.75456275.js → column-views.00c19fe8.js} +2 -2
  82. package/dist/node/{column-views.75456275.js.map → column-views.00c19fe8.js.map} +1 -1
  83. package/dist/node/{column-views.7c322202.js → column-views.00caa49a.js} +2 -2
  84. package/dist/node/{column-views.7c322202.js.map → column-views.00caa49a.js.map} +1 -1
  85. package/dist/node/{column-views.9efa91f1.js → column-views.07680def.js} +2 -2
  86. package/dist/node/{column-views.9efa91f1.js.map → column-views.07680def.js.map} +1 -1
  87. package/dist/node/column-views.0980c2a0.js +2 -0
  88. package/dist/node/column-views.0980c2a0.js.map +1 -0
  89. package/dist/node/column-views.1495397b.js +2 -0
  90. package/dist/node/column-views.1495397b.js.map +1 -0
  91. package/dist/node/{column-views.47440961.js → column-views.190241c4.js} +2 -2
  92. package/dist/node/{column-views.47440961.js.map → column-views.190241c4.js.map} +1 -1
  93. package/dist/node/{column-views.fafbc028.js → column-views.1d1d6117.js} +2 -2
  94. package/dist/node/{column-views.fafbc028.js.map → column-views.1d1d6117.js.map} +1 -1
  95. package/dist/node/column-views.26306041.js +2 -0
  96. package/dist/node/column-views.26306041.js.map +1 -0
  97. package/dist/node/{column-views.6556e004.js → column-views.3ce34544.js} +2 -2
  98. package/dist/node/{column-views.6556e004.js.map → column-views.3ce34544.js.map} +1 -1
  99. package/dist/node/{column-views.c007152b.js → column-views.41d72280.js} +2 -2
  100. package/dist/node/{column-views.c007152b.js.map → column-views.41d72280.js.map} +1 -1
  101. package/dist/node/{column-views.59ce629d.js → column-views.4e677bc4.js} +2 -2
  102. package/dist/node/{column-views.59ce629d.js.map → column-views.4e677bc4.js.map} +1 -1
  103. package/dist/node/{column-views.2fe8feb9.js → column-views.60dc38f3.js} +2 -2
  104. package/dist/node/{column-views.2fe8feb9.js.map → column-views.60dc38f3.js.map} +1 -1
  105. package/dist/node/{column-views.d818b387.js → column-views.68fe4814.js} +2 -2
  106. package/dist/node/{column-views.d818b387.js.map → column-views.68fe4814.js.map} +1 -1
  107. package/dist/node/{column-views.3a947d58.js → column-views.69ddf14d.js} +2 -2
  108. package/dist/node/{column-views.3a947d58.js.map → column-views.69ddf14d.js.map} +1 -1
  109. package/dist/node/column-views.6d6a8fde.js +2 -0
  110. package/dist/node/column-views.6d6a8fde.js.map +1 -0
  111. package/dist/node/{column-views.08bef1f6.js → column-views.705f66b1.js} +2 -2
  112. package/dist/node/{column-views.08bef1f6.js.map → column-views.705f66b1.js.map} +1 -1
  113. package/dist/node/{column-views.8e846c57.js → column-views.7714173b.js} +2 -2
  114. package/dist/node/{column-views.8e846c57.js.map → column-views.7714173b.js.map} +1 -1
  115. package/dist/node/{column-views.a14f0134.js → column-views.7ee01d2b.js} +2 -2
  116. package/dist/node/{column-views.a14f0134.js.map → column-views.7ee01d2b.js.map} +1 -1
  117. package/dist/node/column-views.861b8e3e.js +2 -0
  118. package/dist/node/column-views.861b8e3e.js.map +1 -0
  119. package/dist/node/{column-views.c921fa1a.js → column-views.89afd6b3.js} +2 -2
  120. package/dist/node/{column-views.c921fa1a.js.map → column-views.89afd6b3.js.map} +1 -1
  121. package/dist/node/{column-views.8eae7028.js → column-views.8f5532b0.js} +2 -2
  122. package/dist/node/{column-views.8eae7028.js.map → column-views.8f5532b0.js.map} +1 -1
  123. package/dist/node/{column-views.67e7a200.js → column-views.97f7f353.js} +2 -2
  124. package/dist/node/{column-views.67e7a200.js.map → column-views.97f7f353.js.map} +1 -1
  125. package/dist/node/{column-views.3d0c2bea.js → column-views.9a408bbf.js} +2 -2
  126. package/dist/node/{column-views.3d0c2bea.js.map → column-views.9a408bbf.js.map} +1 -1
  127. package/dist/node/column-views.a0969fc8.js +2 -0
  128. package/dist/node/column-views.a0969fc8.js.map +1 -0
  129. package/dist/node/{column-views.02869dc5.js → column-views.a0e23210.js} +2 -2
  130. package/dist/node/{column-views.02869dc5.js.map → column-views.a0e23210.js.map} +1 -1
  131. package/dist/node/{column-views.8f540f78.js → column-views.a54c0820.js} +2 -2
  132. package/dist/node/{column-views.8f540f78.js.map → column-views.a54c0820.js.map} +1 -1
  133. package/dist/node/{column-views.0f401891.js → column-views.b3aa406d.js} +2 -2
  134. package/dist/node/{column-views.0f401891.js.map → column-views.b3aa406d.js.map} +1 -1
  135. package/dist/node/{column-views.0a2596c2.js → column-views.b7f69f19.js} +2 -2
  136. package/dist/node/{column-views.0a2596c2.js.map → column-views.b7f69f19.js.map} +1 -1
  137. package/dist/node/{column-views.cd54f1b1.js → column-views.b9900855.js} +2 -2
  138. package/dist/node/{column-views.cd54f1b1.js.map → column-views.b9900855.js.map} +1 -1
  139. package/dist/node/{column-views.6b391d60.js → column-views.bd1481f0.js} +2 -2
  140. package/dist/node/{column-views.6b391d60.js.map → column-views.bd1481f0.js.map} +1 -1
  141. package/dist/node/{column-views.e264a767.js → column-views.bf90c8a3.js} +2 -2
  142. package/dist/node/column-views.bf90c8a3.js.map +1 -0
  143. package/dist/node/{column-views.fee4e65d.js → column-views.bfaf5ddb.js} +2 -2
  144. package/dist/node/{column-views.fee4e65d.js.map → column-views.bfaf5ddb.js.map} +1 -1
  145. package/dist/node/{column-views.00ad98fa.js → column-views.dedafcae.js} +2 -2
  146. package/dist/node/{column-views.00ad98fa.js.map → column-views.dedafcae.js.map} +1 -1
  147. package/dist/node/{column-views.e153680b.js → column-views.e1481ea6.js} +2 -2
  148. package/dist/node/{column-views.e153680b.js.map → column-views.e1481ea6.js.map} +1 -1
  149. package/dist/node/{column-views.c4188442.js → column-views.e4e014cd.js} +2 -2
  150. package/dist/node/{column-views.c4188442.js.map → column-views.e4e014cd.js.map} +1 -1
  151. package/dist/node/{column-views.465e593b.js → column-views.ed7b4c16.js} +2 -2
  152. package/dist/node/{column-views.465e593b.js.map → column-views.ed7b4c16.js.map} +1 -1
  153. package/dist/node/{column-views.2577dec0.js → column-views.f749dbd6.js} +2 -2
  154. package/dist/node/{column-views.2577dec0.js.map → column-views.f749dbd6.js.map} +1 -1
  155. package/dist/node/{column-views.825d2719.js → column-views.fcf2be3c.js} +2 -2
  156. package/dist/node/{column-views.825d2719.js.map → column-views.fcf2be3c.js.map} +1 -1
  157. package/dist/node/index.js +1 -1
  158. package/dist/node/index.js.map +1 -1
  159. package/package.json +5 -4
  160. package/src/age-model-overlay.ts +4 -2
  161. package/src/column.ts +18 -41
  162. package/src/correlation-chart/main.ts +11 -9
  163. package/src/data-provider/core.ts +89 -0
  164. package/src/data-provider/store.ts +79 -88
  165. package/src/data-provider/unit-selection.ts +120 -147
  166. package/src/unit-details/popover.ts +8 -4
  167. package/dist/esm/column-views.4b259f9e.js +0 -136
  168. package/dist/esm/column-views.4b259f9e.js.map +0 -1
  169. package/dist/esm/column-views.6f96d1c3.js.map +0 -1
  170. package/dist/esm/column-views.9d51a5ab.js.map +0 -1
  171. package/dist/esm/column-views.b9265595.js.map +0 -1
  172. package/dist/esm/column-views.c44febd0.js +0 -169
  173. package/dist/esm/column-views.c44febd0.js.map +0 -1
  174. package/dist/esm/column-views.e3f0cd7b.js.map +0 -1
  175. package/dist/node/column-views.0beae29d.js +0 -2
  176. package/dist/node/column-views.0beae29d.js.map +0 -1
  177. package/dist/node/column-views.46efb749.js +0 -2
  178. package/dist/node/column-views.46efb749.js.map +0 -1
  179. package/dist/node/column-views.868c04dc.js +0 -2
  180. package/dist/node/column-views.868c04dc.js.map +0 -1
  181. package/dist/node/column-views.dfe9a971.js +0 -2
  182. package/dist/node/column-views.dfe9a971.js.map +0 -1
  183. package/dist/node/column-views.e264a767.js.map +0 -1
  184. package/dist/node/column-views.ed2fd268.js +0 -2
  185. package/dist/node/column-views.ed2fd268.js.map +0 -1
@@ -0,0 +1,89 @@
1
+ import { atom, type WritableAtom } from "jotai";
2
+ import { createIsolation } from "jotai-scope";
3
+ import { BaseUnit } from "@macrostrat/api-types";
4
+ import { ReactNode, useEffect, useRef } from "react";
5
+ import h from "@macrostrat/hyper";
6
+
7
+ interface StateIsolation {
8
+ Provider: (props: {
9
+ store?: any;
10
+ initialValues?: AtomMap;
11
+ children: ReactNode;
12
+ }) => ReactNode;
13
+ useStore: () => any;
14
+ useAtom: any;
15
+ useAtomValue: any;
16
+ useSetAtom: any;
17
+ }
18
+
19
+ export const scope: StateIsolation = createIsolation();
20
+
21
+ export type AtomMap = [WritableAtom<any, any, any>, any][];
22
+
23
+ type ProviderProps = {
24
+ children: ReactNode;
25
+ atoms?: AtomMap;
26
+ shouldUpdateAtoms?: boolean;
27
+ };
28
+
29
+ export function ScopedProvider({
30
+ children,
31
+ atoms,
32
+ shouldUpdateAtoms = true,
33
+ }: ProviderProps) {
34
+ // Always use the same store instance in this tree
35
+ let val = null;
36
+ try {
37
+ val = scope.useStore();
38
+ // This store has already been provided
39
+ return children;
40
+ } catch {
41
+ return h(scope.Provider, { store: null, initialValues: atoms }, [
42
+ h.if(shouldUpdateAtoms)(AtomUpdater, { atoms }),
43
+ children,
44
+ ]);
45
+ }
46
+ }
47
+
48
+ function AtomUpdater({
49
+ atoms,
50
+ }: {
51
+ atoms: [WritableAtom<any, any, any>, any][];
52
+ }) {
53
+ /**
54
+ * A generic updater to sync Jotai atoms with state passed as props.
55
+ * Useful for scoped providers where state needs to be synced outside
56
+ * the current context.
57
+ */
58
+ /** TODO: this is an awkward way to keep atoms updated */
59
+ // The scoped store
60
+ const store = scope.useStore();
61
+
62
+ // Warn on atoms changing length
63
+ const prevLengthRef = useRef(atoms.length);
64
+ useEffect(() => {
65
+ if (prevLengthRef.current !== atoms.length) {
66
+ console.error("Error: number of atoms in ScopedAtomUpdater has changed.");
67
+ prevLengthRef.current = atoms.length;
68
+ }
69
+ }, [atoms.length]);
70
+
71
+ for (const [atom, value] of atoms) {
72
+ useEffect(() => {
73
+ store.set(atom, value);
74
+ }, [store, value]);
75
+ }
76
+ return null;
77
+ }
78
+
79
+ export const columnUnitsAtom = atom<BaseUnit[]>();
80
+
81
+ export const columnUnitsMapAtom = atom<Map<number, BaseUnit> | null>((get) => {
82
+ const units = get(columnUnitsAtom);
83
+ if (!units) return null;
84
+ const unitMap = new Map<number, BaseUnit>();
85
+ units.forEach((unit) => {
86
+ unitMap.set(unit.unit_id, unit);
87
+ });
88
+ return unitMap;
89
+ });
@@ -1,11 +1,4 @@
1
- import {
2
- createContext,
3
- ReactNode,
4
- useContext,
5
- useEffect,
6
- useMemo,
7
- useRef,
8
- } from "react";
1
+ import { createContext, ReactNode, useContext, useMemo } from "react";
9
2
  import h from "@macrostrat/hyper";
10
3
  import {
11
4
  CompositeColumnScale,
@@ -13,84 +6,98 @@ import {
13
6
  } from "../prepare-units/composite-scale";
14
7
  import { ColumnAxisType } from "@macrostrat/column-components";
15
8
  import type { ExtUnit, PackageLayoutData } from "../prepare-units";
16
- // An isolated jotai store for Macrostrat column usage
17
- // TODO: there might be a better way to do this using the MacrostratDataProvider or similar
18
- import { createIsolation } from "jotai-scope";
19
- import { atom, PrimitiveAtom, type WritableAtom } from "jotai";
20
-
21
- const { Provider, useSetAtom, useAtomValue, useStore } = createIsolation();
22
-
23
- type ProviderProps = {
9
+ import {
10
+ allowUnitSelectionAtom,
11
+ selectedUnitIDAtom,
12
+ UnitSelectionCallbacks,
13
+ UnitSelectionCallbackManager,
14
+ } from "./unit-selection";
15
+ import { BaseUnit } from "@macrostrat/api-types";
16
+ import {
17
+ AtomMap,
18
+ columnUnitsAtom,
19
+ columnUnitsMapAtom,
20
+ scope,
21
+ ScopedProvider,
22
+ } from "./core";
23
+
24
+ export interface ColumnStateProviderProps<
25
+ T extends BaseUnit,
26
+ > extends Partial<UnitSelectionCallbacks> {
24
27
  children: ReactNode;
25
- initialValues?: Iterable<[WritableAtom<any, any, any>, any]>;
26
- };
27
-
28
- const columnUnitsAtom = atom<ExtUnit[]>();
29
-
30
- const columnUnitsMapAtom = atom<Map<number, ExtUnit> | null>((get) => {
31
- const units = get(columnUnitsAtom);
32
- if (!units) return null;
33
- const unitMap = new Map<number, ExtUnit>();
34
- units.forEach((unit) => {
35
- unitMap.set(unit.unit_id, unit);
36
- });
37
- return unitMap;
38
- });
39
-
40
- function ScopedProvider({ children, ...rest }: ProviderProps) {
41
- // Always use the same store instance in this tree
42
- let val = null;
43
- try {
44
- val = useStore();
45
- } catch {
46
- // No store found, create a new one
47
- val = null;
48
- }
49
- return h(Provider, { store: val, ...rest }, children);
28
+ units: T[];
29
+ selectedUnit: number | null;
30
+ allowUnitSelection?: boolean;
50
31
  }
51
32
 
52
- export function MacrostratColumnStateProvider({
33
+ export function MacrostratColumnStateProvider<T extends BaseUnit>({
53
34
  children,
54
35
  units,
55
- }: {
56
- children?: ReactNode;
57
- units: ExtUnit[];
58
- }) {
36
+ allowUnitSelection = false,
37
+ onUnitSelected,
38
+ selectedUnit,
39
+ }: ColumnStateProviderProps<T>) {
59
40
  /** Top-level provider for Macrostrat column data.
60
41
  * It is either provided by the Column component itself, or
61
42
  * can be hoisted higher in the tree to provide a common data context
62
43
  */
63
44
 
64
- const atomMap: [[PrimitiveAtom<ExtUnit[]>, ExtUnit[]]] = [
45
+ /* By default, unit selection is disabled. However, if any related props are passed,
46
+ we enable it.
47
+ */
48
+ const _allowSelection = useMemo(() => {
49
+ if (allowUnitSelection) {
50
+ return true;
51
+ }
52
+ return selectedUnit != null || onUnitSelected != null;
53
+ }, []);
54
+
55
+ const atomMap: AtomMap = [
65
56
  [columnUnitsAtom, units],
57
+ [allowUnitSelectionAtom, _allowSelection],
58
+ [selectedUnitIDAtom, selectedUnit],
66
59
  ];
67
60
 
61
+ let selectionHandlers: ReactNode = null;
62
+ if (_allowSelection) {
63
+ selectionHandlers = h(UnitSelectionCallbackManager, { onUnitSelected });
64
+ }
65
+
68
66
  return h(
69
67
  ScopedProvider,
70
68
  {
71
- initialValues: atomMap,
69
+ atoms: atomMap,
72
70
  },
73
- [h(AtomUpdater, { atoms: atomMap }), children],
71
+ [selectionHandlers, children],
74
72
  );
75
73
  }
76
74
 
77
- export interface MacrostratColumnDataContext {
78
- units: ExtUnit[];
75
+ export interface MacrostratColumnDataContext<T extends BaseUnit> {
76
+ units: T[];
79
77
  sections: PackageLayoutData[];
80
78
  totalHeight?: number;
81
79
  axisType?: ColumnAxisType;
80
+ allowUnitSelection?: boolean;
81
+ }
82
+
83
+ export interface ColumnDataProviderProps<T extends BaseUnit>
84
+ extends MacrostratColumnDataContext<T>, ColumnStateProviderProps<T> {
85
+ children: ReactNode;
82
86
  }
83
87
 
84
88
  const MacrostratColumnDataContext =
85
- createContext<MacrostratColumnDataContext>(null);
89
+ createContext<MacrostratColumnDataContext<any>>(null);
86
90
 
87
- export function MacrostratColumnDataProvider({
91
+ export function MacrostratColumnDataProvider<T extends BaseUnit>({
88
92
  children,
89
93
  units,
90
94
  sections,
91
95
  totalHeight,
92
96
  axisType,
93
- }: MacrostratColumnDataContext & { children: ReactNode }) {
97
+ allowUnitSelection,
98
+ onUnitSelected,
99
+ selectedUnit,
100
+ }: ColumnDataProviderProps<T>) {
94
101
  /** Internal provider for Macrostrat column data.
95
102
  * As a general rule, we want to provide data and column-axis
96
103
  * height calculations through the context, since these need to
@@ -114,11 +121,26 @@ export function MacrostratColumnDataProvider({
114
121
 
115
122
  return h(
116
123
  MacrostratColumnStateProvider,
117
- { units },
124
+ {
125
+ units,
126
+ allowUnitSelection,
127
+ onUnitSelected,
128
+ selectedUnit,
129
+ },
118
130
  h(MacrostratColumnDataContext.Provider, { value }, children),
119
131
  );
120
132
  }
121
133
 
134
+ export function UnitSelectionProvider(props: ColumnStateProviderProps<any>) {
135
+ /* A basic unit selection provider without column data context.
136
+ Mostly for use with the CorrelationChart component.
137
+ */
138
+ return h(MacrostratColumnStateProvider, {
139
+ ...props,
140
+ allowUnitSelection: true,
141
+ });
142
+ }
143
+
122
144
  export function useMacrostratColumnData() {
123
145
  const ctx = useContext(MacrostratColumnDataContext);
124
146
  if (!ctx) {
@@ -130,12 +152,12 @@ export function useMacrostratColumnData() {
130
152
  }
131
153
 
132
154
  export function useMacrostratUnits() {
133
- return useAtomValue(columnUnitsAtom);
155
+ return scope.useAtomValue(columnUnitsAtom);
134
156
  }
135
157
 
136
158
  export function useColumnUnitsMap(): Map<number, ExtUnit> | null {
137
159
  try {
138
- return useAtomValue(columnUnitsMapAtom);
160
+ return scope.useAtomValue(columnUnitsMapAtom) as Map<number, ExtUnit>;
139
161
  } catch {
140
162
  return null;
141
163
  }
@@ -148,34 +170,3 @@ export function useCompositeScale(): CompositeColumnScale {
148
170
  [ctx.sections],
149
171
  );
150
172
  }
151
-
152
- function AtomUpdater({
153
- atoms,
154
- }: {
155
- atoms: [WritableAtom<any, any, any>, any][];
156
- }) {
157
- /**
158
- * A generic updater to sync Jotai atoms with state passed as props.
159
- * Useful for scoped providers where state needs to be synced outside
160
- * of the current context.
161
- */
162
- /** TODO: this is an awkward way to keep atoms updated */
163
- // The scoped store
164
- const store = useStore();
165
-
166
- // Warn on atoms changing length
167
- const prevLengthRef = useRef(atoms.length);
168
- useEffect(() => {
169
- if (prevLengthRef.current !== atoms.length) {
170
- console.error("Error: number of atoms in ScopedAtomUpdater has changed.");
171
- prevLengthRef.current = atoms.length;
172
- }
173
- }, [atoms.length]);
174
-
175
- for (const [atom, value] of atoms) {
176
- useEffect(() => {
177
- store.set(atom, value);
178
- }, [store, value]);
179
- }
180
- return null;
181
- }
@@ -1,55 +1,21 @@
1
1
  import { BaseUnit } from "@macrostrat/api-types";
2
- import h from "@macrostrat/hyper";
3
2
  import { useKeyHandler } from "@macrostrat/ui-components";
4
- import {
5
- createContext,
6
- useContext,
7
- useEffect,
8
- useState,
9
- ReactNode,
10
- RefObject,
11
- useRef,
12
- useCallback,
13
- } from "react";
14
- import { createStore, StoreApi, useStore } from "zustand";
3
+ import { useEffect, useRef, useCallback } from "react";
15
4
  import type { RectBounds, IUnit } from "../units/types";
5
+ import { atom } from "jotai";
6
+ import { columnUnitsMapAtom, scope } from "./core";
16
7
 
17
8
  type UnitSelectDispatch = (
18
9
  unit: number | BaseUnit | null,
19
10
  target: HTMLElement | null,
20
- event: Event | null,
21
11
  ) => void;
22
12
 
23
- const UnitSelectionContext = createContext<StoreApi<UnitSelectionStore>>(null);
24
-
25
13
  export function useUnitSelectionDispatch(): UnitSelectDispatch {
26
- const store = useContext(UnitSelectionContext);
27
- if (store == null) {
28
- return () => {};
29
- }
30
- return useStore(store, (state) => state.onUnitSelected);
31
- }
32
-
33
- export function useUnitSelectionStore<T>(
34
- selector: (state: UnitSelectionStore) => T,
35
- ): T {
36
- const store = useContext(UnitSelectionContext);
37
- if (store == null) {
38
- return null;
39
- }
40
- return useStore(store, selector);
14
+ return scope.useSetAtom(selectedUnitAtom);
41
15
  }
42
16
 
43
17
  export function useSelectedUnit() {
44
- return useUnitSelectionStore((state) => state.selectedUnitData);
45
- }
46
-
47
- interface UnitSelectionStore {
48
- selectedUnit: number | null;
49
- selectedUnitData: BaseUnit | null;
50
- overlayPosition: RectBounds | null;
51
- onUnitSelected: UnitSelectDispatch;
52
- setSelectedUnit: (unit: number | null) => void;
18
+ return scope.useAtomValue(selectedUnitAtom);
53
19
  }
54
20
 
55
21
  export interface ColumnClickData {
@@ -60,134 +26,141 @@ export interface ColumnClickData {
60
26
  // Room for boundary IDs eventually
61
27
  }
62
28
 
63
- export function UnitSelectionProvider<T extends BaseUnit>(props: {
64
- children: ReactNode;
65
- columnRef?: RefObject<HTMLElement>;
66
- units: T[];
67
- selectedUnit: number | null;
29
+ export interface UnitSelectionCallbacks {
30
+ // It's sort of unfortunate that we need to pass in the column ref here
68
31
  onClickedColumn?: (columnClickData: ColumnClickData, event: Event) => void;
69
- onUnitSelected?: (unitID: number | null, unit: T | null) => void;
70
- }) {
71
- const [store] = useState(() =>
72
- createStore<UnitSelectionStore>((set) => ({
73
- selectedUnit: null,
74
- selectedUnitData: null,
75
- overlayPosition: null,
76
- setSelectedUnit(selectedUnit: number | null | BaseUnit) {
77
- if (selectedUnit == null) {
78
- set({ selectedUnit: null, selectedUnitData: null });
79
- return;
80
- // If it's a number, set the selected unit
81
- } else if (typeof selectedUnit === "number") {
82
- set({ selectedUnit, selectedUnitData: null });
83
- } else if ("unit_id" in selectedUnit) {
84
- set({
85
- selectedUnit: selectedUnit.unit_id,
86
- selectedUnitData: selectedUnit,
87
- });
88
- }
89
- },
90
- onUnitSelected: (
91
- input: number | T | null,
92
- target: HTMLElement = null,
93
- event: PointerEvent = null,
94
- ) => {
95
- if (input == null) {
96
- props.onUnitSelected?.(null, null);
97
- return set({
98
- selectedUnit: null,
99
- selectedUnitData: null,
100
- overlayPosition: null,
101
- });
102
- }
103
-
104
- let unit: T | null = null;
105
- if (typeof input === "number") {
106
- unit = props.units.find((u) => u.unit_id === input) || null;
107
- } else {
108
- unit = input;
109
- }
110
-
111
- const el = props.columnRef?.current;
112
- let overlayPosition = null;
113
-
114
- /** This is not the natural place to get positions within the column,
115
- * but it will work for now.
116
- */
117
- if (props.onClickedColumn) {
118
- // Infer height from top and bottom height of unit (because that's passed back with the call)
119
- //const py = event.y;
120
- //const [bottom, top] = getUnitHeightRange(unit, axisType);
121
- //const height = Math.abs(bottom - top);
122
-
123
- /** Ideally this would be defined at the column level */
124
- const columnClickData: ColumnClickData = {
125
- unitID: unit?.unit_id,
126
- unit,
127
- target,
128
- height: el?.getBoundingClientRect().height || 0,
129
- };
130
- props.onClickedColumn(columnClickData, event);
131
- }
132
-
133
- if (unit != null && el != null && target != null) {
134
- const rect = el.getBoundingClientRect();
135
- const targetRect = target.getBoundingClientRect();
136
-
137
- overlayPosition = {
138
- x: targetRect.left - rect.left,
139
- y: targetRect.top - rect.top,
140
- width: targetRect.width,
141
- height: targetRect.height,
142
- };
143
- }
144
- props.onUnitSelected?.(unit?.unit_id, unit);
145
-
146
- return set({
147
- selectedUnit: unit?.unit_id,
148
- selectedUnitData: unit,
149
- overlayPosition,
150
- });
151
- },
152
- })),
153
- );
32
+ onUnitSelected?: <T extends BaseUnit>(
33
+ unitID: number | null,
34
+ unit: T | null,
35
+ ) => void;
36
+ }
154
37
 
155
- const { units, selectedUnit } = props;
38
+ export const allowUnitSelectionAtom = atom<boolean>(true);
156
39
 
157
- useEffect(() => {
158
- // Synchronize store with provided props
159
- if (selectedUnit != null) {
160
- const unitData = units?.find((u) => u.unit_id === selectedUnit);
161
- store.setState({ selectedUnit, selectedUnitData: unitData });
162
- } else {
163
- store.setState({ selectedUnit: null, selectedUnitData: null });
40
+ export const selectedUnitIDAtom = atom<number | null>();
41
+
42
+ const overlayPositionAtom = atom<RectBounds | null>();
43
+
44
+ const columnRefAtom = atom<{ current: HTMLElement | null }>({ current: null });
45
+
46
+ export function useColumnRef() {
47
+ return scope.useAtomValue(columnRefAtom);
48
+ }
49
+
50
+ const selectedUnitAtom = atom(
51
+ (get) => {
52
+ if (!get(allowUnitSelectionAtom)) return null;
53
+
54
+ const unitID = get(selectedUnitIDAtom);
55
+ if (unitID == null) return null;
56
+ const unitsMap = get(columnUnitsMapAtom);
57
+ return unitsMap?.get(unitID) || null;
58
+ },
59
+ (
60
+ get,
61
+ set,
62
+ selectedUnit: number | BaseUnit | null,
63
+ target: HTMLElement | null = null,
64
+ ): BaseUnit | null => {
65
+ if (!get(allowUnitSelectionAtom)) {
66
+ throw new Error("Unit selection is disabled.");
67
+ }
68
+
69
+ let unitID: number | null;
70
+ let unit: BaseUnit | null = null;
71
+ if (selectedUnit == null) {
72
+ unitID = null;
73
+ } else if (typeof selectedUnit === "number") {
74
+ unitID = selectedUnit;
75
+ } else if ("unit_id" in selectedUnit) {
76
+ unitID = selectedUnit.unit_id;
77
+ }
78
+
79
+ if (unitID != null) {
80
+ // Verify that the unit exists in the current colum, else throw
81
+ const unitsMap = get(columnUnitsMapAtom);
82
+ if (!unitsMap?.has(unitID)) {
83
+ throw new Error(
84
+ `Unit with ID ${unitID} not found in current column units.`,
85
+ );
86
+ }
87
+ unit = unitsMap.get(unitID) ?? null;
88
+ }
89
+
90
+ let overlayPosition: RectBounds | null = null;
91
+
92
+ const columnEl = get(columnRefAtom)?.current;
93
+
94
+ if (unit != null && columnEl != null && target != null) {
95
+ const rect = columnEl.getBoundingClientRect();
96
+ const targetRect = target.getBoundingClientRect();
97
+ overlayPosition = {
98
+ x: targetRect.left - rect.left,
99
+ y: targetRect.top - rect.top,
100
+ width: targetRect.width,
101
+ height: targetRect.height,
102
+ };
164
103
  }
165
- }, [selectedUnit, units]);
166
104
 
167
- return h(UnitSelectionContext.Provider, { value: store }, props.children);
105
+ set(selectedUnitIDAtom, unitID);
106
+ set(overlayPositionAtom, overlayPosition);
107
+
108
+ return unit;
109
+ },
110
+ );
111
+
112
+ export function useAtomOverlayPosition() {
113
+ return scope.useAtomValue(overlayPositionAtom);
114
+ }
115
+
116
+ export function UnitSelectionCallbackManager({
117
+ onUnitSelected,
118
+ }: UnitSelectionCallbacks) {
119
+ const selectedUnit = scope.useAtomValue(selectedUnitAtom);
120
+ useEffect(() => {
121
+ onUnitSelected?.(selectedUnit?.unit_id ?? null, selectedUnit ?? null);
122
+ }, [selectedUnit?.unit_id]);
123
+ return null;
168
124
  }
169
125
 
126
+ /** TODO: figure out if onClickedColumn is still needed
127
+ /** This is not the natural place to get positions within the column,
128
+ * but it will work for now.
129
+ if (props.onClickedColumn) {
130
+ // Infer height from top and bottom height of unit (because that's passed back with the call)
131
+ //const py = event.y;
132
+ //const [bottom, top] = getUnitHeightRange(unit, axisType);
133
+ //const height = Math.abs(bottom - top);
134
+
135
+ const columnClickData: ColumnClickData = {
136
+ unitID: unit?.unit_id,
137
+ unit,
138
+ target,
139
+ height: el?.getBoundingClientRect().height || 0,
140
+ };
141
+ props.onClickedColumn(columnClickData, event);
142
+ }*/
143
+
170
144
  export function useUnitSelectionTarget(
171
145
  unit: IUnit,
172
146
  ): [React.RefObject<HTMLElement>, boolean, (evt: Event) => void] {
173
147
  const ref = useRef<HTMLElement>();
174
148
  const selectedUnit = useSelectedUnit();
175
- const onSelectUnit = useUnitSelectionStore((state) => state.onUnitSelected);
149
+ const selectUnit = useUnitSelectionDispatch();
176
150
  const selected = selectedUnit?.unit_id == unit.unit_id;
177
151
 
178
152
  const onClick = useCallback(
179
153
  (evt: Event) => {
180
- if (onSelectUnit == null) return;
181
- onSelectUnit(unit, ref.current, evt);
154
+ selectUnit?.(unit, ref.current);
182
155
  evt.stopPropagation();
183
156
  },
184
- [unit, onSelectUnit],
157
+ [unit, selectUnit],
185
158
  );
186
159
 
187
160
  useEffect(() => {
188
- if (!selected || onSelectUnit == null) return;
161
+ if (!selected || selectUnit == null) return;
189
162
  // In case we haven't set the position of the unit (if we don't have a target), set the selected unit
190
- onSelectUnit(unit, ref.current, null);
163
+ selectUnit(unit, ref.current);
191
164
 
192
165
  // Scroll the unit into view
193
166
  ref.current?.scrollIntoView({
@@ -195,7 +168,7 @@ export function useUnitSelectionTarget(
195
168
  block: "center",
196
169
  inline: "nearest",
197
170
  });
198
- }, [selected, onSelectUnit]);
171
+ }, [selected, selectUnit]);
199
172
 
200
173
  return [ref, selected, onClick];
201
174
  }
@@ -219,7 +192,7 @@ export function UnitKeyboardNavigation<T extends BaseUnit>({
219
192
  (event) => {
220
193
  const nextIx = keyMap[event.keyCode];
221
194
  if (nextIx == null || nextIx < 0 || nextIx >= units.length) return;
222
- selectUnit(units[nextIx], null, null);
195
+ selectUnit(units[nextIx], null);
223
196
  event.stopPropagation();
224
197
  },
225
198
  [units, ix],
@@ -1,7 +1,11 @@
1
1
  import hyper from "@macrostrat/hyper";
2
2
  import { Popover } from "@blueprintjs/core";
3
3
  import styles from "./popover.module.sass";
4
- import { useSelectedUnit, useUnitSelectionStore } from "../data-provider";
4
+ import {
5
+ useAtomOverlayPosition,
6
+ useSelectedUnit,
7
+ useUnitSelectionDispatch,
8
+ } from "../data-provider";
5
9
  import { UnitDetailsPanel } from "./panel";
6
10
 
7
11
  const h = hyper.styled(styles);
@@ -54,8 +58,8 @@ function InteractionBarrier({ children }) {
54
58
 
55
59
  export function UnitSelectionPopover() {
56
60
  const unit = useSelectedUnit();
57
- const selectUnit = useUnitSelectionStore((state) => state.onUnitSelected);
58
- const position = useUnitSelectionStore((state) => state.overlayPosition);
61
+ const selectUnit = useUnitSelectionDispatch();
62
+ const position = useAtomOverlayPosition();
59
63
  if (unit == null) {
60
64
  return null;
61
65
  }
@@ -79,7 +83,7 @@ export function UnitSelectionPopover() {
79
83
  className: "legend-panel",
80
84
  onSelectUnit: (id: number) => {
81
85
  console.log("Selected unit in popover:", id);
82
- selectUnit(id, null, null);
86
+ selectUnit(id, null);
83
87
  },
84
88
  }),
85
89
  ),