@genome-spy/core 0.78.0 → 0.79.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 (292) hide show
  1. package/dist/bundle/{browser-KWU9rWZT.js → browser-CETrb2cm.js} +53 -33
  2. package/dist/bundle/esm-BdLYkz-m.js +248 -0
  3. package/dist/bundle/esm-BwiDsqSb.js +1367 -0
  4. package/dist/bundle/esm-CDFd1cjk.js +441 -0
  5. package/dist/bundle/{esm-DVOHLB1e.js → esm-CTUHLDbv.js} +30 -30
  6. package/dist/bundle/{esm-NIYEaYkc.js → esm-Cx-EbkOj.js} +13 -13
  7. package/dist/bundle/esm-DlYGqi79.js +128 -0
  8. package/dist/bundle/{esm-BygJiwh0.js → esm-k9p3oHkt.js} +133 -158
  9. package/dist/bundle/{esm-CT3ygiMq.js → esm-zAZJQO6D.js} +226 -212
  10. package/dist/bundle/index.es.js +14879 -11656
  11. package/dist/bundle/index.js +119 -108
  12. package/dist/bundle/{parquetRead-DG_-F5j5.js → parquetRead-Cad1SOVV.js} +473 -399
  13. package/dist/schema.json +18940 -6914
  14. package/dist/src/config/axisConfig.d.ts +2 -2
  15. package/dist/src/config/axisConfig.d.ts.map +1 -1
  16. package/dist/src/config/axisConfig.js +28 -44
  17. package/dist/src/config/configLayers.d.ts +45 -0
  18. package/dist/src/config/configLayers.d.ts.map +1 -0
  19. package/dist/src/config/configLayers.js +110 -0
  20. package/dist/src/config/defaultConfig.d.ts.map +1 -1
  21. package/dist/src/config/defaultConfig.js +8 -1
  22. package/dist/src/config/defaults/legendDefaults.d.ts +14 -0
  23. package/dist/src/config/defaults/legendDefaults.d.ts.map +1 -0
  24. package/dist/src/config/defaults/legendDefaults.js +46 -0
  25. package/dist/src/config/defaults/titleDefaults.d.ts.map +1 -1
  26. package/dist/src/config/defaults/titleDefaults.js +26 -18
  27. package/dist/src/config/legendConfig.d.ts +11 -0
  28. package/dist/src/config/legendConfig.d.ts.map +1 -0
  29. package/dist/src/config/legendConfig.js +63 -0
  30. package/dist/src/config/styleUtils.d.ts +8 -2
  31. package/dist/src/config/styleUtils.d.ts.map +1 -1
  32. package/dist/src/config/styleUtils.js +25 -1
  33. package/dist/src/config/themes.d.ts.map +1 -1
  34. package/dist/src/config/themes.js +21 -2
  35. package/dist/src/config/titleConfig.d.ts.map +1 -1
  36. package/dist/src/config/titleConfig.js +2 -18
  37. package/dist/src/data/collector.d.ts.map +1 -1
  38. package/dist/src/data/collector.js +40 -18
  39. package/dist/src/data/flowInit.d.ts +6 -0
  40. package/dist/src/data/flowInit.d.ts.map +1 -1
  41. package/dist/src/data/flowInit.js +1 -1
  42. package/dist/src/data/flowNode.d.ts +32 -0
  43. package/dist/src/data/flowNode.d.ts.map +1 -1
  44. package/dist/src/data/flowNode.js +59 -0
  45. package/dist/src/data/sources/lazy/bamSource.d.ts +0 -1
  46. package/dist/src/data/sources/lazy/bamSource.d.ts.map +1 -1
  47. package/dist/src/data/sources/lazy/bamSource.js +39 -30
  48. package/dist/src/data/sources/lazy/bigBedSource.d.ts +0 -10
  49. package/dist/src/data/sources/lazy/bigBedSource.d.ts.map +1 -1
  50. package/dist/src/data/sources/lazy/bigBedSource.js +127 -62
  51. package/dist/src/data/sources/lazy/bigWigSource.d.ts +2 -2
  52. package/dist/src/data/sources/lazy/bigWigSource.d.ts.map +1 -1
  53. package/dist/src/data/sources/lazy/bigWigSource.js +234 -81
  54. package/dist/src/data/sources/lazy/gff3Source.d.ts +7 -3
  55. package/dist/src/data/sources/lazy/gff3Source.d.ts.map +1 -1
  56. package/dist/src/data/sources/lazy/gff3Source.js +7 -8
  57. package/dist/src/data/sources/lazy/indexedFastaSource.d.ts +1 -1
  58. package/dist/src/data/sources/lazy/indexedFastaSource.d.ts.map +1 -1
  59. package/dist/src/data/sources/lazy/indexedFastaSource.js +28 -19
  60. package/dist/src/data/sources/lazy/legendEntriesSource.d.ts +24 -0
  61. package/dist/src/data/sources/lazy/legendEntriesSource.d.ts.map +1 -0
  62. package/dist/src/data/sources/lazy/legendEntriesSource.js +218 -0
  63. package/dist/src/data/sources/lazy/legendGradientSource.d.ts +30 -0
  64. package/dist/src/data/sources/lazy/legendGradientSource.d.ts.map +1 -0
  65. package/dist/src/data/sources/lazy/legendGradientSource.js +388 -0
  66. package/dist/src/data/sources/lazy/mockLazySource.d.ts +4 -1
  67. package/dist/src/data/sources/lazy/mockLazySource.d.ts.map +1 -1
  68. package/dist/src/data/sources/lazy/mockLazySource.js +49 -4
  69. package/dist/src/data/sources/lazy/registerCoreLazySources.js +2 -0
  70. package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
  71. package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +3 -4
  72. package/dist/src/data/sources/lazy/tabixSource.d.ts +9 -4
  73. package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
  74. package/dist/src/data/sources/lazy/tabixSource.js +201 -70
  75. package/dist/src/data/sources/lazy/tabixTsvSource.d.ts +2 -3
  76. package/dist/src/data/sources/lazy/tabixTsvSource.d.ts.map +1 -1
  77. package/dist/src/data/sources/lazy/tabixTsvSource.js +14 -12
  78. package/dist/src/data/sources/lazy/vcfSource.d.ts +7 -3
  79. package/dist/src/data/sources/lazy/vcfSource.d.ts.map +1 -1
  80. package/dist/src/data/sources/lazy/vcfSource.js +7 -8
  81. package/dist/src/data/sources/urlDescriptor.d.ts +165 -0
  82. package/dist/src/data/sources/urlDescriptor.d.ts.map +1 -0
  83. package/dist/src/data/sources/urlDescriptor.js +473 -0
  84. package/dist/src/data/sources/urlDescriptorController.d.ts +25 -0
  85. package/dist/src/data/sources/urlDescriptorController.d.ts.map +1 -0
  86. package/dist/src/data/sources/urlDescriptorController.js +72 -0
  87. package/dist/src/data/sources/urlDescriptorState.d.ts +47 -0
  88. package/dist/src/data/sources/urlDescriptorState.d.ts.map +1 -0
  89. package/dist/src/data/sources/urlDescriptorState.js +129 -0
  90. package/dist/src/data/sources/urlSource.d.ts.map +1 -1
  91. package/dist/src/data/sources/urlSource.js +101 -61
  92. package/dist/src/data/transforms/packLegendLabels.d.ts +21 -0
  93. package/dist/src/data/transforms/packLegendLabels.d.ts.map +1 -0
  94. package/dist/src/data/transforms/packLegendLabels.js +189 -0
  95. package/dist/src/data/transforms/transformFactory.d.ts.map +1 -1
  96. package/dist/src/data/transforms/transformFactory.js +4 -0
  97. package/dist/src/data/transforms/truncateText.d.ts +27 -0
  98. package/dist/src/data/transforms/truncateText.d.ts.map +1 -0
  99. package/dist/src/data/transforms/truncateText.js +94 -0
  100. package/dist/src/debug/dataflowDebugSnapshot.d.ts +58 -0
  101. package/dist/src/debug/dataflowDebugSnapshot.d.ts.map +1 -0
  102. package/dist/src/debug/dataflowDebugSnapshot.js +159 -0
  103. package/dist/src/debug/markDebugSnapshot.d.ts +54 -0
  104. package/dist/src/debug/markDebugSnapshot.d.ts.map +1 -0
  105. package/dist/src/debug/markDebugSnapshot.js +100 -0
  106. package/dist/src/debug/paramDebugSnapshot.d.ts +53 -0
  107. package/dist/src/debug/paramDebugSnapshot.d.ts.map +1 -0
  108. package/dist/src/debug/paramDebugSnapshot.js +86 -0
  109. package/dist/src/debug/resolutionDebugSnapshot.d.ts +155 -0
  110. package/dist/src/debug/resolutionDebugSnapshot.d.ts.map +1 -0
  111. package/dist/src/debug/resolutionDebugSnapshot.js +291 -0
  112. package/dist/src/debug/valuePreview.d.ts +9 -0
  113. package/dist/src/debug/valuePreview.d.ts.map +1 -0
  114. package/dist/src/debug/valuePreview.js +57 -0
  115. package/dist/src/debug/viewDebugSnapshot.d.ts +131 -0
  116. package/dist/src/debug/viewDebugSnapshot.d.ts.map +1 -0
  117. package/dist/src/debug/viewDebugSnapshot.js +390 -0
  118. package/dist/src/embedFactory.d.ts.map +1 -1
  119. package/dist/src/embedFactory.js +6 -1
  120. package/dist/src/encoder/encoder.d.ts +2 -2
  121. package/dist/src/encoder/encoder.d.ts.map +1 -1
  122. package/dist/src/encoder/encoder.js +5 -4
  123. package/dist/src/fonts/bmFontManager.d.ts +1 -1
  124. package/dist/src/fonts/bmFontManager.d.ts.map +1 -1
  125. package/dist/src/fonts/bmFontManager.js +45 -10
  126. package/dist/src/fonts/textMetrics.d.ts +69 -0
  127. package/dist/src/fonts/textMetrics.d.ts.map +1 -0
  128. package/dist/src/fonts/textMetrics.js +73 -0
  129. package/dist/src/genomeSpy/headlessBootstrap.d.ts.map +1 -1
  130. package/dist/src/genomeSpy/headlessBootstrap.js +6 -0
  131. package/dist/src/genomeSpy/renderCoordinator.d.ts.map +1 -1
  132. package/dist/src/genomeSpy/renderCoordinator.js +25 -3
  133. package/dist/src/genomeSpy/viewDataInit.d.ts +14 -0
  134. package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -1
  135. package/dist/src/genomeSpy/viewDataInit.js +45 -8
  136. package/dist/src/genomeSpyBase.d.ts +6 -0
  137. package/dist/src/genomeSpyBase.d.ts.map +1 -1
  138. package/dist/src/genomeSpyBase.js +20 -3
  139. package/dist/src/gl/glslScaleGenerator.d.ts +17 -0
  140. package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
  141. package/dist/src/gl/glslScaleGenerator.js +39 -2
  142. package/dist/src/gl/includes/common.glsl.js +1 -1
  143. package/dist/src/gl/vertexRangeIndex.d.ts.map +1 -1
  144. package/dist/src/gl/vertexRangeIndex.js +4 -2
  145. package/dist/src/gl/webGLHelper.d.ts +1 -1
  146. package/dist/src/gl/webGLHelper.d.ts.map +1 -1
  147. package/dist/src/gl/webGLHelper.js +13 -8
  148. package/dist/src/marks/__snapshots__/shaderSnapshot.test.js.snap +140 -3
  149. package/dist/src/marks/mark.d.ts +47 -4
  150. package/dist/src/marks/mark.d.ts.map +1 -1
  151. package/dist/src/marks/mark.js +158 -54
  152. package/dist/src/marks/point.d.ts.map +1 -1
  153. package/dist/src/marks/point.js +4 -0
  154. package/dist/src/marks/point.vertex.glsl.js +1 -1
  155. package/dist/src/marks/text.d.ts +1 -1
  156. package/dist/src/marks/text.d.ts.map +1 -1
  157. package/dist/src/marks/text.js +2 -7
  158. package/dist/src/marks/text.vertex.glsl.js +1 -1
  159. package/dist/src/paramRuntime/paramUtils.d.ts +43 -9
  160. package/dist/src/paramRuntime/paramUtils.d.ts.map +1 -1
  161. package/dist/src/paramRuntime/paramUtils.js +61 -1
  162. package/dist/src/paramRuntime/viewParamRuntime.d.ts +32 -0
  163. package/dist/src/paramRuntime/viewParamRuntime.d.ts.map +1 -1
  164. package/dist/src/paramRuntime/viewParamRuntime.js +63 -0
  165. package/dist/src/scales/axisResolution.d.ts +35 -0
  166. package/dist/src/scales/axisResolution.d.ts.map +1 -1
  167. package/dist/src/scales/axisResolution.js +115 -7
  168. package/dist/src/scales/legendResolution.d.ts +83 -0
  169. package/dist/src/scales/legendResolution.d.ts.map +1 -0
  170. package/dist/src/scales/legendResolution.js +461 -0
  171. package/dist/src/scales/scaleResolution.d.ts +36 -0
  172. package/dist/src/scales/scaleResolution.d.ts.map +1 -1
  173. package/dist/src/scales/scaleResolution.js +59 -0
  174. package/dist/src/scales/viewLevelGuideConfig.d.ts +53 -0
  175. package/dist/src/scales/viewLevelGuideConfig.d.ts.map +1 -0
  176. package/dist/src/scales/viewLevelGuideConfig.js +224 -0
  177. package/dist/src/scales/viewLevelScaleConfig.d.ts.map +1 -1
  178. package/dist/src/scales/viewLevelScaleConfig.js +13 -2
  179. package/dist/src/spec/axis.d.ts +109 -3
  180. package/dist/src/spec/channel.d.ts +23 -4
  181. package/dist/src/spec/config.d.ts +59 -4
  182. package/dist/src/spec/data.d.ts +177 -17
  183. package/dist/src/spec/legend.d.ts +246 -0
  184. package/dist/src/spec/mark.d.ts +16 -4
  185. package/dist/src/spec/title.d.ts +58 -1
  186. package/dist/src/spec/transform.d.ts +149 -0
  187. package/dist/src/spec/view.d.ts +39 -6
  188. package/dist/src/types/embedApi.d.ts +262 -6
  189. package/dist/src/types/rendering.d.ts +19 -3
  190. package/dist/src/types/viewContext.d.ts +18 -2
  191. package/dist/src/utils/arrayUtils.d.ts +11 -0
  192. package/dist/src/utils/arrayUtils.d.ts.map +1 -1
  193. package/dist/src/utils/arrayUtils.js +23 -0
  194. package/dist/src/utils/suspension.d.ts +17 -0
  195. package/dist/src/utils/suspension.d.ts.map +1 -0
  196. package/dist/src/utils/suspension.js +41 -0
  197. package/dist/src/view/axisGridView.d.ts.map +1 -1
  198. package/dist/src/view/axisGridView.js +1 -4
  199. package/dist/src/view/axisView.d.ts +18 -2
  200. package/dist/src/view/axisView.d.ts.map +1 -1
  201. package/dist/src/view/axisView.js +180 -75
  202. package/dist/src/view/concatView.d.ts +10 -2
  203. package/dist/src/view/concatView.d.ts.map +1 -1
  204. package/dist/src/view/concatView.js +46 -9
  205. package/dist/src/view/containerMutationHelper.d.ts +20 -1
  206. package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
  207. package/dist/src/view/containerMutationHelper.js +196 -33
  208. package/dist/src/view/facetView.d.ts +1 -1
  209. package/dist/src/view/gridView/gridChild.d.ts +54 -4
  210. package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
  211. package/dist/src/view/gridView/gridChild.js +301 -120
  212. package/dist/src/view/gridView/gridChildLegends.d.ts +57 -0
  213. package/dist/src/view/gridView/gridChildLegends.d.ts.map +1 -0
  214. package/dist/src/view/gridView/gridChildLegends.js +503 -0
  215. package/dist/src/view/gridView/gridView.d.ts +25 -0
  216. package/dist/src/view/gridView/gridView.d.ts.map +1 -1
  217. package/dist/src/view/gridView/gridView.js +490 -78
  218. package/dist/src/view/gridView/legendLayout.d.ts +30 -0
  219. package/dist/src/view/gridView/legendLayout.d.ts.map +1 -0
  220. package/dist/src/view/gridView/legendLayout.js +115 -0
  221. package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
  222. package/dist/src/view/gridView/scrollbar.js +1 -4
  223. package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
  224. package/dist/src/view/gridView/selectionRect.js +1 -4
  225. package/dist/src/view/gridView/separatorView.d.ts.map +1 -1
  226. package/dist/src/view/gridView/separatorView.js +1 -4
  227. package/dist/src/view/layerView.d.ts +9 -2
  228. package/dist/src/view/layerView.d.ts.map +1 -1
  229. package/dist/src/view/layerView.js +18 -1
  230. package/dist/src/view/layout/flexLayout.d.ts +20 -4
  231. package/dist/src/view/layout/flexLayout.d.ts.map +1 -1
  232. package/dist/src/view/layout/flexLayout.js +331 -31
  233. package/dist/src/view/layout/rectangle.d.ts +14 -0
  234. package/dist/src/view/layout/rectangle.d.ts.map +1 -1
  235. package/dist/src/view/layout/rectangle.js +40 -0
  236. package/dist/src/view/legend/legendEntries.d.ts +20 -0
  237. package/dist/src/view/legend/legendEntries.d.ts.map +1 -0
  238. package/dist/src/view/legend/legendEntries.js +21 -0
  239. package/dist/src/view/legendView.d.ts +137 -0
  240. package/dist/src/view/legendView.d.ts.map +1 -0
  241. package/dist/src/view/legendView.js +1654 -0
  242. package/dist/src/view/renderingContext/bufferedViewRenderingContext.d.ts.map +1 -1
  243. package/dist/src/view/renderingContext/bufferedViewRenderingContext.js +26 -4
  244. package/dist/src/view/renderingContext/clipOptions.d.ts +44 -0
  245. package/dist/src/view/renderingContext/clipOptions.d.ts.map +1 -0
  246. package/dist/src/view/renderingContext/clipOptions.js +140 -0
  247. package/dist/src/view/renderingContext/simpleViewRenderingContext.d.ts.map +1 -1
  248. package/dist/src/view/renderingContext/simpleViewRenderingContext.js +12 -1
  249. package/dist/src/view/resolutionPlanner.d.ts +2 -1
  250. package/dist/src/view/resolutionPlanner.d.ts.map +1 -1
  251. package/dist/src/view/resolutionPlanner.js +89 -25
  252. package/dist/src/view/testUtils.d.ts +4 -2
  253. package/dist/src/view/testUtils.d.ts.map +1 -1
  254. package/dist/src/view/testUtils.js +60 -7
  255. package/dist/src/view/titleView.d.ts +37 -0
  256. package/dist/src/view/titleView.d.ts.map +1 -0
  257. package/dist/src/view/titleView.js +584 -0
  258. package/dist/src/view/unitView.d.ts +3 -3
  259. package/dist/src/view/unitView.d.ts.map +1 -1
  260. package/dist/src/view/unitView.js +3 -2
  261. package/dist/src/view/view.d.ts +25 -24
  262. package/dist/src/view/view.d.ts.map +1 -1
  263. package/dist/src/view/view.js +126 -16
  264. package/dist/src/view/viewChrome.d.ts +33 -0
  265. package/dist/src/view/viewChrome.d.ts.map +1 -0
  266. package/dist/src/view/viewChrome.js +64 -0
  267. package/dist/src/view/viewFactory.d.ts +2 -5
  268. package/dist/src/view/viewFactory.d.ts.map +1 -1
  269. package/dist/src/view/viewFactory.js +1 -2
  270. package/dist/src/view/viewIdentityRegistry.d.ts +37 -0
  271. package/dist/src/view/viewIdentityRegistry.d.ts.map +1 -0
  272. package/dist/src/view/viewIdentityRegistry.js +71 -0
  273. package/dist/src/view/viewMutationAcidTestUtils.d.ts +112 -0
  274. package/dist/src/view/viewMutationAcidTestUtils.d.ts.map +1 -0
  275. package/dist/src/view/viewMutationAcidTestUtils.js +234 -0
  276. package/dist/src/view/viewMutationApi.d.ts +42 -0
  277. package/dist/src/view/viewMutationApi.d.ts.map +1 -0
  278. package/dist/src/view/viewMutationApi.js +811 -0
  279. package/dist/src/view/viewSelectors.d.ts +11 -9
  280. package/dist/src/view/viewSelectors.d.ts.map +1 -1
  281. package/dist/src/view/viewSelectors.js +28 -17
  282. package/package.json +4 -4
  283. package/dist/bundle/esm-CuMSzCHy.js +0 -298
  284. package/dist/bundle/esm-DAnOffpD.js +0 -1426
  285. package/dist/bundle/esm-DMXpJXM4.js +0 -369
  286. package/dist/bundle/esm-DNtC3H80.js +0 -121
  287. package/dist/src/view/title.d.ts +0 -13
  288. package/dist/src/view/title.d.ts.map +0 -1
  289. package/dist/src/view/title.js +0 -154
  290. /package/dist/bundle/{AbortablePromiseCache-3gHJdF3E.js → AbortablePromiseCache-BTmAcN-t.js} +0 -0
  291. /package/dist/bundle/{esm-CuVa5T98.js → esm-VvpZ9hsq.js} +0 -0
  292. /package/dist/bundle/{chunk-DmhlhrBa.js → rolldown-runtime-Dy4uBu1J.js} +0 -0
@@ -2,6 +2,8 @@ import { primaryPositionalChannels } from "../../encoder/encoder.js";
2
2
  import {
3
3
  FlexDimensions,
4
4
  getLargestSize,
5
+ getSizeDefMaxPx,
6
+ getSizeDefMinPx,
5
7
  mapToPixelCoords,
6
8
  parseSizeDef,
7
9
  ZERO_SIZEDEF,
@@ -9,7 +11,11 @@ import {
9
11
  import Grid from "../layout/grid.js";
10
12
  import Padding from "../layout/padding.js";
11
13
  import Rectangle from "../layout/rectangle.js";
12
- import AxisView, { CHANNEL_ORIENTS, ORIENT_CHANNELS } from "../axisView.js";
14
+ import AxisView, {
15
+ CHANNEL_ORIENTS,
16
+ ORIENT_CHANNELS,
17
+ getExternalAxisOverhang,
18
+ } from "../axisView.js";
13
19
  import ContainerView from "../containerView.js";
14
20
  import {
15
21
  propagateInteraction,
@@ -20,9 +26,26 @@ import UnitView from "../unitView.js";
20
26
  import { interactionToZoom } from "../zoom.js";
21
27
  import GridChild from "./gridChild.js";
22
28
  import KeyboardZoomController from "./keyboardZoomController.js";
29
+ import { renderLocalLegends } from "./legendLayout.js";
30
+ import {
31
+ addLegendView,
32
+ createGridChildLegend,
33
+ disposeLegendViews,
34
+ getLegendOverhang,
35
+ getOrderedLegendEntries,
36
+ iterateLegendViews,
37
+ isActiveLegendRegion,
38
+ } from "./gridChildLegends.js";
23
39
  import SeparatorView, { resolveSeparatorProps } from "./separatorView.js";
24
40
  import { getZoomableResolutions } from "./zoomNavigationUtils.js";
41
+ import { moveArrayItem } from "../../utils/arrayUtils.js";
25
42
  import { isHConcatSpec, isVConcatSpec } from "../viewSpecGuards.js";
43
+ import {
44
+ clipCoords,
45
+ combineClipOptions,
46
+ createClipOptions,
47
+ normalizeClipOptions,
48
+ } from "../renderingContext/clipOptions.js";
26
49
 
27
50
  // Secondary ordering within a z-index bucket for GridView-owned decorations.
28
51
  // These are not z-indices themselves: actual layering is decided first by the
@@ -34,6 +57,7 @@ const DECORATION_ORDER = Object.freeze({
34
57
  grid: 20,
35
58
  backgroundStroke: 30,
36
59
  axis: 40,
60
+ legend: 50,
37
61
  selectionRect: 80,
38
62
  scrollbar: 90,
39
63
  title: 100,
@@ -44,6 +68,80 @@ const DECORATION_ORDER = Object.freeze({
44
68
  // letting an explicit user zindex override the default.
45
69
  const CLIPPED_DECORATION_ZINDEX = 10;
46
70
 
71
+ /**
72
+ * Legends are rendered as guide regions, not grid children. Their thickness is
73
+ * handled as overhang, but their parallel min/max constraints still affect the
74
+ * grid dimension that the parent concat sees.
75
+ *
76
+ * @param {import("./gridChildLegends.js").GridChildLegends} legends
77
+ * @returns {FlexDimensions}
78
+ */
79
+ function getLegendParallelSizeConstraints(legends) {
80
+ /** @type {import("../layout/flexLayout.js").SizeDef[]} */
81
+ const widths = [];
82
+ /** @type {import("../layout/flexLayout.js").SizeDef[]} */
83
+ const heights = [];
84
+
85
+ for (const [orient, region] of Object.entries(legends)) {
86
+ if (!isActiveLegendRegion(region)) {
87
+ continue;
88
+ }
89
+
90
+ const size = region.legendView.getSize();
91
+ if (orient == "top" || orient == "bottom") {
92
+ widths.push(size.width);
93
+ } else {
94
+ // Side gradients can fill the available viewport height, but that
95
+ // available height must be determined by the real grid children and
96
+ // top/bottom chrome. Otherwise a shared right/left legend can make
97
+ // the grid grow to the browser height and then fill that height.
98
+ heights.push({ px: getSizeDefMinPx(size.height), grow: 0 });
99
+ }
100
+ }
101
+
102
+ return new FlexDimensions(getLargestSize(widths), getLargestSize(heights));
103
+ }
104
+
105
+ /**
106
+ * Treat an explicit concat/grid size as preferred available space while still
107
+ * letting fixed children and guide chrome establish a larger minimum. This
108
+ * mirrors flexbox behavior more closely than treating the explicit size as a
109
+ * hard clipping bound.
110
+ *
111
+ * @param {import("../layout/flexLayout.js").SizeDef} preferred
112
+ * @param {import("../layout/flexLayout.js").SizeDef} content
113
+ * @returns {import("../layout/flexLayout.js").SizeDef}
114
+ */
115
+ function combinePreferredAndContentSize(preferred, content) {
116
+ const preferredGrow = preferred.grow ?? 0;
117
+ const preferredPx = preferred.px ?? 0;
118
+ const minPx = Math.max(
119
+ getSizeDefMinPx(preferred),
120
+ getSizeDefMinPx(content)
121
+ );
122
+
123
+ if (!preferredGrow) {
124
+ return { px: Math.max(preferredPx, minPx), grow: 0 };
125
+ }
126
+
127
+ /** @type {import("../layout/flexLayout.js").SizeDef} */
128
+ const size = {
129
+ px: Math.max(preferredPx, content.px ?? 0),
130
+ grow: preferredGrow,
131
+ };
132
+
133
+ if (minPx > (size.px ?? 0)) {
134
+ size.minPx = minPx;
135
+ }
136
+
137
+ const preferredMaxPx = getSizeDefMaxPx(preferred);
138
+ if (preferredMaxPx !== undefined && preferredMaxPx >= minPx) {
139
+ size.maxPx = preferredMaxPx;
140
+ }
141
+
142
+ return size;
143
+ }
144
+
47
145
  /**
48
146
  * Modeled after: https://vega.github.io/vega/docs/layout/
49
147
  *
@@ -97,6 +195,9 @@ export default class GridView extends ContainerView {
97
195
  */
98
196
  #sharedAxes = {};
99
197
 
198
+ /** @type {import("./gridChildLegends.js").GridChildLegends} */
199
+ #sharedLegends = {};
200
+
100
201
  #childSerial = 0;
101
202
 
102
203
  /** @type {Partial<Record<"horizontal" | "vertical", SeparatorView>>} */
@@ -223,6 +324,17 @@ export default class GridView extends ContainerView {
223
324
  this.invalidateSizeCache();
224
325
  }
225
326
 
327
+ /**
328
+ * Moves a child within the grid without disposing it.
329
+ *
330
+ * @param {number} fromIndex
331
+ * @param {number} index Destination index after temporarily removing the child.
332
+ */
333
+ moveChildAt(fromIndex, index) {
334
+ moveArrayItem(this.#children, fromIndex, index);
335
+ this.invalidateSizeCache();
336
+ }
337
+
226
338
  get #visibleChildren() {
227
339
  return this.#children.filter((gridChild) =>
228
340
  gridChild.view.isConfiguredVisible()
@@ -275,10 +387,25 @@ export default class GridView extends ContainerView {
275
387
  * @protected
276
388
  */
277
389
  async createAxes() {
390
+ await this.syncGuideViews();
391
+ }
392
+
393
+ /**
394
+ * Recreates guide and chrome views that depend on the child hierarchy.
395
+ * Shared guides always depend on the whole container. Grid-child guides can
396
+ * be limited to newly inserted children during mutations.
397
+ *
398
+ * @param {{ gridChildren?: GridChild[] }} [options]
399
+ */
400
+ async syncGuideViews(options = {}) {
401
+ const gridChildren = options.gridChildren ?? this.#children;
402
+
278
403
  await this.syncSharedAxes();
404
+ await this.syncSharedLegends();
279
405
  await Promise.all(
280
- this.#children.map((gridChild) => gridChild.createAxes())
406
+ gridChildren.map((gridChild) => gridChild.createAxes())
281
407
  );
408
+ this.invalidateSizeCache();
282
409
  }
283
410
 
284
411
  /**
@@ -325,6 +452,25 @@ export default class GridView extends ContainerView {
325
452
  await Promise.all(promises);
326
453
  }
327
454
 
455
+ /**
456
+ * Recreates shared legends based on current legend resolutions.
457
+ *
458
+ * Shared legends are GridView-owned for the same reason as shared axes:
459
+ * their placement is relative to the whole child grid, not any individual
460
+ * GridChild.
461
+ */
462
+ async syncSharedLegends() {
463
+ disposeLegendViews(this.#sharedLegends);
464
+ this.#sharedLegends = {};
465
+
466
+ for (const { definition, resolution } of getOrderedLegendEntries([
467
+ this,
468
+ ])) {
469
+ const legend = await createGridChildLegend(definition, this);
470
+ await addLegendView(this.#sharedLegends, legend, resolution);
471
+ }
472
+ }
473
+
328
474
  /**
329
475
  * @returns {IterableIterator<View>}
330
476
  */
@@ -340,6 +486,8 @@ export default class GridView extends ContainerView {
340
486
  for (const axisView of Object.values(this.#sharedAxes)) {
341
487
  yield axisView;
342
488
  }
489
+
490
+ yield* iterateLegendViews(this.#sharedLegends);
343
491
  }
344
492
 
345
493
  /**
@@ -367,17 +515,48 @@ export default class GridView extends ContainerView {
367
515
  })
368
516
  .reduce((a, b) => Math.max(a, b), 0);
369
517
 
518
+ /**
519
+ * @param {GridChild} child
520
+ * @returns {import("../layout/flexLayout.js").SizeDef}
521
+ */
522
+ const getPlotSize = (child) => {
523
+ // External overhang is represented by axis/padding slots. The
524
+ // growable view slot should contain only the child's plot area.
525
+ const size = child.view.getViewportSize()[dim];
526
+ const overhang = child.view.getOverhang();
527
+ const overhangSize =
528
+ direction == "column" ? overhang.width : overhang.height;
529
+
530
+ const plotSize = {
531
+ px: Math.max((size.px ?? 0) - overhangSize, 0),
532
+ grow: size.grow,
533
+ minPx:
534
+ size.minPx === undefined
535
+ ? undefined
536
+ : Math.max(size.minPx - overhangSize, 0),
537
+ maxPx:
538
+ size.maxPx === undefined
539
+ ? undefined
540
+ : Math.max(size.maxPx - overhangSize, 0),
541
+ };
542
+
543
+ const legendSize = getLegendParallelSizeConstraints(child.legends);
544
+ // Side legends constrain row height, while top/bottom legends
545
+ // constrain column width. Their perpendicular size is overhang.
546
+ return getLargestSize([
547
+ plotSize,
548
+ direction == "column" ? legendSize.width : legendSize.height,
549
+ ]);
550
+ };
551
+
370
552
  return this._cache(`size/directionSizes/${direction}`, () =>
371
553
  this.#grid[direction == "column" ? "colIndices" : "rowIndices"].map(
372
554
  (col) => ({
373
555
  axisBefore: getMaxAxisSize(col, 0),
374
556
  axisAfter: getMaxAxisSize(col, 1),
375
557
  view: getLargestSize(
376
- col.map(
377
- (rowIndex) =>
378
- this.#visibleChildren[
379
- rowIndex
380
- ].view.getViewportSize()[dim]
558
+ col.map((rowIndex) =>
559
+ getPlotSize(this.#visibleChildren[rowIndex])
381
560
  )
382
561
  ),
383
562
  })
@@ -448,12 +627,21 @@ export default class GridView extends ContainerView {
448
627
  #getFlexSize(direction) {
449
628
  let grow = 0;
450
629
  let px = 0;
630
+ let minPx = 0;
631
+ let maxPx = 0;
632
+ let hasMaxPx = true;
451
633
 
452
634
  const explicitSize =
453
635
  direction == "row" ? this.spec.height : this.spec.width;
454
- if (explicitSize || explicitSize === 0) {
455
- return parseSizeDef(explicitSize);
456
- }
636
+ const preferredSize =
637
+ explicitSize || explicitSize === 0
638
+ ? parseSizeDef(explicitSize)
639
+ : undefined;
640
+ const usePreferredAsViewSlot =
641
+ preferredSize &&
642
+ (direction == "column"
643
+ ? this.#grid.colIndices.length == 1
644
+ : this.#grid.rowIndices.length == 1);
457
645
 
458
646
  const sizes = this.#getSizes(direction);
459
647
 
@@ -461,6 +649,8 @@ export default class GridView extends ContainerView {
461
649
  if (i > 0) {
462
650
  // Spacing
463
651
  px += this.#spacing;
652
+ minPx += this.#spacing;
653
+ maxPx += this.#spacing;
464
654
  }
465
655
 
466
656
  if (i == 0 || this.wrappingFacet) {
@@ -470,13 +660,28 @@ export default class GridView extends ContainerView {
470
660
 
471
661
  // Axis/padding
472
662
  px += size.axisBefore;
663
+ minPx += size.axisBefore;
664
+ maxPx += size.axisBefore;
473
665
 
474
666
  // View
475
- px += size.view.px ?? 0;
476
- grow += size.view.grow ?? 0;
667
+ const viewSize = usePreferredAsViewSlot
668
+ ? combinePreferredAndContentSize(preferredSize, size.view)
669
+ : size.view;
670
+ px += viewSize.px ?? 0;
671
+ grow += viewSize.grow ?? 0;
672
+ minPx += getSizeDefMinPx(viewSize);
673
+
674
+ const viewMaxPx = getSizeDefMaxPx(viewSize);
675
+ if (viewMaxPx === undefined) {
676
+ hasMaxPx = false;
677
+ } else {
678
+ maxPx += viewMaxPx;
679
+ }
477
680
 
478
681
  // Axis/padding
479
682
  px += size.axisAfter;
683
+ minPx += size.axisAfter;
684
+ maxPx += size.axisAfter;
480
685
 
481
686
  if (i == sizes.length - 1 || this.wrappingFacet) {
482
687
  //Footer
@@ -484,7 +689,16 @@ export default class GridView extends ContainerView {
484
689
  }
485
690
  }
486
691
 
487
- return { px, grow };
692
+ const measuredSize = {
693
+ px,
694
+ grow,
695
+ minPx: minPx || undefined,
696
+ maxPx: sizes.length && hasMaxPx ? maxPx : undefined,
697
+ };
698
+
699
+ return preferredSize && !usePreferredAsViewSlot
700
+ ? combinePreferredAndContentSize(preferredSize, measuredSize)
701
+ : measuredSize;
488
702
  }
489
703
 
490
704
  /**
@@ -505,7 +719,7 @@ export default class GridView extends ContainerView {
505
719
  * @return {Padding}
506
720
  */
507
721
  getOverhang() {
508
- return this.#getGridOverhang().union(this.#getSharedAxisOverhang());
722
+ return this.#getGridOverhang().add(this.#getSharedGuideOverhang());
509
723
  }
510
724
 
511
725
  #getGridOverhang() {
@@ -535,11 +749,7 @@ export default class GridView extends ContainerView {
535
749
  return 0;
536
750
  }
537
751
 
538
- return Math.max(
539
- axisView.getPerpendicularSize() +
540
- (axisView.axisProps.offset ?? 0),
541
- 0
542
- );
752
+ return getExternalAxisOverhang(axisView);
543
753
  };
544
754
 
545
755
  return new Padding(
@@ -550,16 +760,58 @@ export default class GridView extends ContainerView {
550
760
  );
551
761
  }
552
762
 
763
+ #getSharedLegendOverhang() {
764
+ const getSharedLegendSize = (
765
+ /** @type {import("../../spec/legend.js").LegendOrient} */ orient
766
+ ) => getLegendOverhang(this.#sharedLegends, orient);
767
+
768
+ return new Padding(
769
+ getSharedLegendSize("top"),
770
+ getSharedLegendSize("right"),
771
+ getSharedLegendSize("bottom"),
772
+ getSharedLegendSize("left")
773
+ );
774
+ }
775
+
776
+ #getSharedGuideOverhang() {
777
+ return this.#getSharedAxisOverhang().add(
778
+ this.#getSharedLegendOverhang()
779
+ );
780
+ }
781
+
782
+ #getSharedAxesByOrient() {
783
+ /** @type {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisView>>} */
784
+ const axes = {};
785
+ for (const axisView of Object.values(this.#sharedAxes)) {
786
+ axes[axisView.axisProps.orient] = axisView;
787
+ }
788
+
789
+ return axes;
790
+ }
791
+
553
792
  /**
554
793
  * @returns {FlexDimensions}
555
794
  */
556
795
  getSize() {
557
- return this._cache("size", () =>
558
- new FlexDimensions(
559
- this.#getFlexSize("column"),
560
- this.#getFlexSize("row")
561
- ).addPadding(this.#getSharedAxisOverhang())
562
- );
796
+ return this._cache("size", () => {
797
+ const parallelLegendSize = getLegendParallelSizeConstraints(
798
+ this.#sharedLegends
799
+ );
800
+
801
+ // Shared legends are placed around the whole grid, so combine their
802
+ // parallel constraints with the child-grid size before adding
803
+ // guide overhang.
804
+ return new FlexDimensions(
805
+ getLargestSize([
806
+ this.#getFlexSize("column"),
807
+ parallelLegendSize.width,
808
+ ]),
809
+ getLargestSize([
810
+ this.#getFlexSize("row"),
811
+ parallelLegendSize.height,
812
+ ])
813
+ ).addPadding(this.#getSharedGuideOverhang());
814
+ });
563
815
  }
564
816
 
565
817
  /**
@@ -578,7 +830,8 @@ export default class GridView extends ContainerView {
578
830
  // Usually padding is applied by the parent GridView, but if this is the root view, we need to apply it here
579
831
  coords = coords.shrink(this.getPadding());
580
832
  }
581
- coords = coords.shrink(this.#getSharedAxisOverhang());
833
+ const sharedGuideOverhang = this.#getSharedGuideOverhang();
834
+ coords = coords.shrink(sharedGuideOverhang);
582
835
 
583
836
  context.pushView(this, coords);
584
837
 
@@ -587,7 +840,7 @@ export default class GridView extends ContainerView {
587
840
  const flexOpts = {
588
841
  devicePixelRatio,
589
842
  };
590
- const columnFlexCoords = mapToPixelCoords(
843
+ let columnFlexCoords = mapToPixelCoords(
591
844
  this.#makeFlexItems("column"),
592
845
  coords.width,
593
846
  flexOpts
@@ -604,6 +857,33 @@ export default class GridView extends ContainerView {
604
857
  this.#columns ?? Infinity
605
858
  );
606
859
 
860
+ let columnLayoutDirty = false;
861
+ for (const [i, gridChild] of this.#visibleChildren.entries()) {
862
+ const [col, row] = grid.getCellCoords(i);
863
+ const colLocSize =
864
+ columnFlexCoords[this.#getViewSlot("column", col)];
865
+ const rowLocSize = rowFlexCoords[this.#getViewSlot("row", row)];
866
+
867
+ // Some child views have side overhang that depends on the final
868
+ // row height, such as SampleView's repeated y-axis threshold. Row
869
+ // slots are known here, but final columns may need one more pass if
870
+ // a child reports that its height-dependent overhang changed.
871
+ const layoutChanged =
872
+ /** @type {{ prepareLayoutSize?: (width: number, height: number) => boolean }} */ (
873
+ gridChild.view
874
+ ).prepareLayoutSize?.(colLocSize.size, rowLocSize.size);
875
+ columnLayoutDirty ||= layoutChanged === true;
876
+ }
877
+
878
+ if (columnLayoutDirty) {
879
+ this._invalidateCacheByPrefix("size/directionSizes/column");
880
+ columnFlexCoords = mapToPixelCoords(
881
+ this.#makeFlexItems("column"),
882
+ coords.width,
883
+ flexOpts
884
+ );
885
+ }
886
+
607
887
  /** @param {number} x */
608
888
  const round = (x) =>
609
889
  Math.round(x * devicePixelRatio) / devicePixelRatio;
@@ -641,16 +921,36 @@ export default class GridView extends ContainerView {
641
921
  /**
642
922
  * @param {FlexDimensions} size
643
923
  * @param {"width" | "height"} dimension
924
+ * @param {boolean} explicitViewport
644
925
  */
645
- const getLen = (size, dimension) =>
646
- (size[dimension].grow
647
- ? (dimension == "width" ? colLocSize : rowLocSize).size
648
- : size[dimension].px) + overhang[dimension];
649
-
650
- const viewportWidth = getLen(viewportSize, "width");
651
- const viewportHeight = getLen(viewportSize, "height");
652
- const viewWidth = getLen(viewSize, "width");
653
- const viewHeight = getLen(viewSize, "height");
926
+ const getLen = (size, dimension, explicitViewport = false) =>
927
+ explicitViewport
928
+ ? size[dimension].grow
929
+ ? (dimension == "width" ? colLocSize : rowLocSize).size
930
+ : size[dimension].px
931
+ : (dimension == "width" ? colLocSize : rowLocSize).size +
932
+ overhang[dimension];
933
+
934
+ const viewportWidth = getLen(
935
+ viewportSize,
936
+ "width",
937
+ view.spec.viewportWidth != null
938
+ );
939
+ const viewportHeight = getLen(
940
+ viewportSize,
941
+ "height",
942
+ view.spec.viewportHeight != null
943
+ );
944
+ const viewWidth = getLen(
945
+ viewSize,
946
+ "width",
947
+ view.spec.viewportWidth != null
948
+ );
949
+ const viewHeight = getLen(
950
+ viewSize,
951
+ "height",
952
+ view.spec.viewportHeight != null
953
+ );
654
954
 
655
955
  const hScrollbar = gridChild.scrollbars.horizontal;
656
956
  const vScrollbar = gridChild.scrollbars.vertical;
@@ -684,9 +984,8 @@ export default class GridView extends ContainerView {
684
984
 
685
985
  gridChild.coords = viewportCoords;
686
986
 
687
- const clippedChildCoords = options.clipRect
688
- ? viewportCoords.intersect(options.clipRect)
689
- : viewportCoords;
987
+ const parentClip = normalizeClipOptions(options);
988
+ const visibleChildCoords = clipCoords(viewportCoords, parentClip);
690
989
 
691
990
  renderItems.push({
692
991
  col,
@@ -700,7 +999,8 @@ export default class GridView extends ContainerView {
700
999
  selectionRect,
701
1000
  viewportCoords,
702
1001
  viewCoords,
703
- clippedChildCoords,
1002
+ parentClip,
1003
+ visibleChildCoords,
704
1004
  viewWidth,
705
1005
  viewHeight,
706
1006
  scrollable,
@@ -755,7 +1055,7 @@ export default class GridView extends ContainerView {
755
1055
  () =>
756
1056
  item.background?.render(
757
1057
  context,
758
- item.clippedChildCoords,
1058
+ item.visibleChildCoords,
759
1059
  {
760
1060
  ...options,
761
1061
  clipRect: undefined,
@@ -809,7 +1109,8 @@ export default class GridView extends ContainerView {
809
1109
  selectionRect,
810
1110
  viewportCoords,
811
1111
  viewCoords,
812
- clippedChildCoords,
1112
+ parentClip,
1113
+ visibleChildCoords,
813
1114
  viewWidth,
814
1115
  viewHeight,
815
1116
  scrollable,
@@ -818,7 +1119,9 @@ export default class GridView extends ContainerView {
818
1119
  row,
819
1120
  } = item;
820
1121
 
821
- const clipped = isClippedChildren(view) || scrollable;
1122
+ const clippedChildren = isClippedChildren(view);
1123
+ const clipped = clippedChildren || scrollable;
1124
+ const clippedDecorations = hasClippedChildren(view) || scrollable;
822
1125
 
823
1126
  for (const gridLineView of Object.values(gridLines)) {
824
1127
  queueDecoration(
@@ -828,6 +1131,19 @@ export default class GridView extends ContainerView {
828
1131
  );
829
1132
  }
830
1133
 
1134
+ const childClip = clipped
1135
+ ? combineClipOptions(
1136
+ parentClip,
1137
+ createClipOptions(
1138
+ visibleChildCoords,
1139
+ clippedChildren ||
1140
+ Boolean(gridChild.scrollbars.horizontal),
1141
+ clippedChildren ||
1142
+ Boolean(gridChild.scrollbars.vertical)
1143
+ )
1144
+ )
1145
+ : options.clip;
1146
+
831
1147
  const renderContent = () =>
832
1148
  view.render(
833
1149
  context,
@@ -835,7 +1151,8 @@ export default class GridView extends ContainerView {
835
1151
  clipped
836
1152
  ? {
837
1153
  ...options,
838
- clipRect: clippedChildCoords,
1154
+ clipRect: childClip?.rect,
1155
+ clip: childClip,
839
1156
  }
840
1157
  : options
841
1158
  );
@@ -846,11 +1163,11 @@ export default class GridView extends ContainerView {
846
1163
  queueDecoration(
847
1164
  defaultBackgroundStrokeZindex(
848
1165
  gridChild.backgroundStrokeZindex,
849
- clipped
1166
+ clippedDecorations
850
1167
  ),
851
1168
  DECORATION_ORDER.backgroundStroke,
852
1169
  () =>
853
- backgroundStroke?.render(context, clippedChildCoords, {
1170
+ backgroundStroke?.render(context, visibleChildCoords, {
854
1171
  ...options,
855
1172
  clipRect: undefined,
856
1173
  })
@@ -888,39 +1205,51 @@ export default class GridView extends ContainerView {
888
1205
  axisView
889
1206
  );
890
1207
 
891
- let clipRect = options.clipRect;
1208
+ let clip = normalizeClipOptions(options);
1209
+ let clipRect = clip?.rect;
892
1210
 
893
1211
  // Scrollable axes must be clipped along the scroll direction.
894
1212
  if (scrollable) {
895
- clipRect = translatedCoords.intersect(clipRect).intersect(
896
- scrollable
897
- ? viewportCoords.modify(
898
- // Ugly hack. Need to implement intersectX and intersectY.
899
- direction == "vertical"
900
- ? {
901
- x: -100000,
902
- width: 200000,
903
- }
904
- : {
905
- y: -100000,
906
- height: 200000,
907
- }
908
- )
909
- : undefined
1213
+ const axisClip = createClipOptions(
1214
+ viewportCoords,
1215
+ direction == "horizontal",
1216
+ direction == "vertical"
1217
+ );
1218
+ clip = combineClipOptions(clip, axisClip);
1219
+ clipRect = clip?.rect;
1220
+ }
1221
+
1222
+ if (clip && axisView.labelClipPolicy === "anchor") {
1223
+ clip = createClipOptions(
1224
+ clip.rect,
1225
+ ORIENT_CHANNELS[orient] === "x",
1226
+ ORIENT_CHANNELS[orient] === "y"
910
1227
  );
1228
+ clipRect = clip?.rect;
911
1229
  }
912
1230
 
913
1231
  queueDecoration(
914
- defaultAxisZindex(axisView.axisProps.zindex, clipped),
1232
+ defaultAxisZindex(axisView.axisProps, clippedDecorations),
915
1233
  DECORATION_ORDER.axis,
916
1234
  () =>
917
1235
  axisView.render(context, translatedCoords, {
918
1236
  ...options,
919
1237
  clipRect,
1238
+ clip,
920
1239
  })
921
1240
  );
922
1241
  }
923
1242
 
1243
+ renderLocalLegends(
1244
+ gridChild.legends,
1245
+ this.#getLegendOffsetAxes(axes, col, row, grid),
1246
+ viewportCoords,
1247
+ context,
1248
+ options,
1249
+ queueDecoration,
1250
+ DECORATION_ORDER.legend
1251
+ );
1252
+
924
1253
  // Axes shared between children
925
1254
  // TODO: What if some have scrollable viewports?
926
1255
  // Should throw an error because cannot have shared axes in such cases.
@@ -934,7 +1263,10 @@ export default class GridView extends ContainerView {
934
1263
  (orient == "bottom" && row == grid.nRows - 1)
935
1264
  ) {
936
1265
  queueDecoration(
937
- defaultAxisZindex(axisView.axisProps.zindex, clipped),
1266
+ defaultAxisZindex(
1267
+ axisView.axisProps,
1268
+ clippedDecorations
1269
+ ),
938
1270
  DECORATION_ORDER.axis,
939
1271
  () =>
940
1272
  axisView.render(
@@ -969,13 +1301,24 @@ export default class GridView extends ContainerView {
969
1301
 
970
1302
  if (title) {
971
1303
  queueDecoration(
972
- gridChild.titleZindex,
1304
+ gridChild.getTitleZindex(),
973
1305
  DECORATION_ORDER.title,
974
- () => title?.render(context, viewportCoords, options)
1306
+ () =>
1307
+ gridChild.renderTitle(context, viewportCoords, options)
975
1308
  );
976
1309
  }
977
1310
  }
978
1311
 
1312
+ renderLocalLegends(
1313
+ this.#sharedLegends,
1314
+ this.#getSharedAxesByOrient(),
1315
+ coords,
1316
+ context,
1317
+ options,
1318
+ queueDecoration,
1319
+ DECORATION_ORDER.legend
1320
+ );
1321
+
979
1322
  renderDecorations(underlays);
980
1323
 
981
1324
  for (const renderContent of contents) {
@@ -987,6 +1330,38 @@ export default class GridView extends ContainerView {
987
1330
  context.popView(this);
988
1331
  }
989
1332
 
1333
+ /**
1334
+ * Local legends are GridChild-owned, but shared axes are GridView-owned.
1335
+ * When they share an orient, the axis should remain next to the plot and
1336
+ * the legend should move outside it. Give legend placement the applicable
1337
+ * shared edge axes so `renderLocalLegends()` can apply the same offset it
1338
+ * already uses for local axes.
1339
+ *
1340
+ * @param {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisView>>} axes
1341
+ * @param {number} col
1342
+ * @param {number} row
1343
+ * @param {Grid} grid
1344
+ */
1345
+ #getLegendOffsetAxes(axes, col, row, grid) {
1346
+ /** @type {Partial<Record<import("../../spec/axis.js").AxisOrient, AxisView>>} */
1347
+ const offsetAxes = { ...axes };
1348
+
1349
+ for (const axisView of Object.values(this.#sharedAxes)) {
1350
+ const orient = axisView.axisProps.orient;
1351
+ const isEdge =
1352
+ (orient == "left" && col == 0) ||
1353
+ (orient == "right" && col == grid.nCols - 1) ||
1354
+ (orient == "top" && row == 0) ||
1355
+ (orient == "bottom" && row == grid.nRows - 1);
1356
+
1357
+ if (isEdge && !offsetAxes[orient]) {
1358
+ offsetAxes[orient] = axisView;
1359
+ }
1360
+ }
1361
+
1362
+ return offsetAxes;
1363
+ }
1364
+
990
1365
  /**
991
1366
  * @param {import("../../utils/interaction.js").default} event
992
1367
  */
@@ -1302,6 +1677,22 @@ export function isClippedChildren(view) {
1302
1677
  return clipped;
1303
1678
  }
1304
1679
 
1680
+ /**
1681
+ * @param {View} view
1682
+ */
1683
+ function hasClippedChildren(view) {
1684
+ let clipped = false;
1685
+
1686
+ view.visit((v) => {
1687
+ if (v instanceof UnitView) {
1688
+ const clip = v.mark.properties.clip;
1689
+ clipped ||= clip === true || clip === "x" || clip === "y";
1690
+ }
1691
+ });
1692
+
1693
+ return clipped;
1694
+ }
1695
+
1305
1696
  /**
1306
1697
  * @param {View} view
1307
1698
  * @returns {boolean}
@@ -1337,15 +1728,24 @@ function getSeparatorDirections(spec) {
1337
1728
  }
1338
1729
 
1339
1730
  /**
1340
- * Default z-index for axes. Clipped or scrollable content gets a higher
1731
+ * Default z-index for axes. Inside axes default to overlays because they share
1732
+ * plot space with marks. Clipped or scrollable outside axes get a higher
1341
1733
  * default to keep guides above visible edge artifacts.
1342
1734
  *
1343
- * @param {number | undefined} zindex
1735
+ * @param {import("../../spec/axis.js").Axis} axisProps
1344
1736
  * @param {boolean} clipped
1345
1737
  * @returns {number}
1346
1738
  */
1347
- function defaultAxisZindex(zindex, clipped) {
1348
- return zindex ?? (clipped ? CLIPPED_DECORATION_ZINDEX : 0);
1739
+ function defaultAxisZindex(axisProps, clipped) {
1740
+ if (axisProps.zindex !== undefined) {
1741
+ return axisProps.zindex;
1742
+ } else if (axisProps.placement === "inside") {
1743
+ return 1;
1744
+ } else if (clipped) {
1745
+ return CLIPPED_DECORATION_ZINDEX;
1746
+ } else {
1747
+ return 0;
1748
+ }
1349
1749
  }
1350
1750
 
1351
1751
  /**
@@ -1369,18 +1769,30 @@ function defaultBackgroundStrokeZindex(zindex, clipped) {
1369
1769
  export function translateAxisCoords(coords, orient, axisView) {
1370
1770
  const props = axisView.axisProps;
1371
1771
  const ps = axisView.getPerpendicularSize();
1772
+ const inside = props.placement === "inside";
1773
+ const offset = props.offset ?? 0;
1372
1774
 
1373
1775
  if (orient == "bottom") {
1374
- return coords
1375
- .translate(0, coords.height + props.offset)
1376
- .modify({ height: ps });
1776
+ return inside
1777
+ ? coords.translate(0, coords.height - ps - offset).modify({
1778
+ height: ps,
1779
+ })
1780
+ : coords.translate(0, coords.height + offset).modify({
1781
+ height: ps,
1782
+ });
1377
1783
  } else if (orient == "top") {
1378
- return coords.translate(0, -ps - props.offset).modify({ height: ps });
1784
+ return inside
1785
+ ? coords.translate(0, offset).modify({ height: ps })
1786
+ : coords.translate(0, -ps - offset).modify({ height: ps });
1379
1787
  } else if (orient == "left") {
1380
- return coords.translate(-ps - props.offset, 0).modify({ width: ps });
1788
+ return inside
1789
+ ? coords.translate(offset, 0).modify({ width: ps })
1790
+ : coords.translate(-ps - offset, 0).modify({ width: ps });
1381
1791
  } else if (orient == "right") {
1382
- return coords
1383
- .translate(coords.width + props.offset, 0)
1384
- .modify({ width: ps });
1792
+ return inside
1793
+ ? coords.translate(coords.width - ps - offset, 0).modify({
1794
+ width: ps,
1795
+ })
1796
+ : coords.translate(coords.width + offset, 0).modify({ width: ps });
1385
1797
  }
1386
1798
  }