@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.
- package/dist/bundle/{browser-KWU9rWZT.js → browser-CETrb2cm.js} +53 -33
- package/dist/bundle/esm-BdLYkz-m.js +248 -0
- package/dist/bundle/esm-BwiDsqSb.js +1367 -0
- package/dist/bundle/esm-CDFd1cjk.js +441 -0
- package/dist/bundle/{esm-DVOHLB1e.js → esm-CTUHLDbv.js} +30 -30
- package/dist/bundle/{esm-NIYEaYkc.js → esm-Cx-EbkOj.js} +13 -13
- package/dist/bundle/esm-DlYGqi79.js +128 -0
- package/dist/bundle/{esm-BygJiwh0.js → esm-k9p3oHkt.js} +133 -158
- package/dist/bundle/{esm-CT3ygiMq.js → esm-zAZJQO6D.js} +226 -212
- package/dist/bundle/index.es.js +14879 -11656
- package/dist/bundle/index.js +119 -108
- package/dist/bundle/{parquetRead-DG_-F5j5.js → parquetRead-Cad1SOVV.js} +473 -399
- package/dist/schema.json +18940 -6914
- package/dist/src/config/axisConfig.d.ts +2 -2
- package/dist/src/config/axisConfig.d.ts.map +1 -1
- package/dist/src/config/axisConfig.js +28 -44
- package/dist/src/config/configLayers.d.ts +45 -0
- package/dist/src/config/configLayers.d.ts.map +1 -0
- package/dist/src/config/configLayers.js +110 -0
- package/dist/src/config/defaultConfig.d.ts.map +1 -1
- package/dist/src/config/defaultConfig.js +8 -1
- package/dist/src/config/defaults/legendDefaults.d.ts +14 -0
- package/dist/src/config/defaults/legendDefaults.d.ts.map +1 -0
- package/dist/src/config/defaults/legendDefaults.js +46 -0
- package/dist/src/config/defaults/titleDefaults.d.ts.map +1 -1
- package/dist/src/config/defaults/titleDefaults.js +26 -18
- package/dist/src/config/legendConfig.d.ts +11 -0
- package/dist/src/config/legendConfig.d.ts.map +1 -0
- package/dist/src/config/legendConfig.js +63 -0
- package/dist/src/config/styleUtils.d.ts +8 -2
- package/dist/src/config/styleUtils.d.ts.map +1 -1
- package/dist/src/config/styleUtils.js +25 -1
- package/dist/src/config/themes.d.ts.map +1 -1
- package/dist/src/config/themes.js +21 -2
- package/dist/src/config/titleConfig.d.ts.map +1 -1
- package/dist/src/config/titleConfig.js +2 -18
- package/dist/src/data/collector.d.ts.map +1 -1
- package/dist/src/data/collector.js +40 -18
- package/dist/src/data/flowInit.d.ts +6 -0
- package/dist/src/data/flowInit.d.ts.map +1 -1
- package/dist/src/data/flowInit.js +1 -1
- package/dist/src/data/flowNode.d.ts +32 -0
- package/dist/src/data/flowNode.d.ts.map +1 -1
- package/dist/src/data/flowNode.js +59 -0
- package/dist/src/data/sources/lazy/bamSource.d.ts +0 -1
- package/dist/src/data/sources/lazy/bamSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bamSource.js +39 -30
- package/dist/src/data/sources/lazy/bigBedSource.d.ts +0 -10
- package/dist/src/data/sources/lazy/bigBedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bigBedSource.js +127 -62
- package/dist/src/data/sources/lazy/bigWigSource.d.ts +2 -2
- package/dist/src/data/sources/lazy/bigWigSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/bigWigSource.js +234 -81
- package/dist/src/data/sources/lazy/gff3Source.d.ts +7 -3
- package/dist/src/data/sources/lazy/gff3Source.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/gff3Source.js +7 -8
- package/dist/src/data/sources/lazy/indexedFastaSource.d.ts +1 -1
- package/dist/src/data/sources/lazy/indexedFastaSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/indexedFastaSource.js +28 -19
- package/dist/src/data/sources/lazy/legendEntriesSource.d.ts +24 -0
- package/dist/src/data/sources/lazy/legendEntriesSource.d.ts.map +1 -0
- package/dist/src/data/sources/lazy/legendEntriesSource.js +218 -0
- package/dist/src/data/sources/lazy/legendGradientSource.d.ts +30 -0
- package/dist/src/data/sources/lazy/legendGradientSource.d.ts.map +1 -0
- package/dist/src/data/sources/lazy/legendGradientSource.js +388 -0
- package/dist/src/data/sources/lazy/mockLazySource.d.ts +4 -1
- package/dist/src/data/sources/lazy/mockLazySource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/mockLazySource.js +49 -4
- package/dist/src/data/sources/lazy/registerCoreLazySources.js +2 -0
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/singleAxisWindowedSource.js +3 -4
- package/dist/src/data/sources/lazy/tabixSource.d.ts +9 -4
- package/dist/src/data/sources/lazy/tabixSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/tabixSource.js +201 -70
- package/dist/src/data/sources/lazy/tabixTsvSource.d.ts +2 -3
- package/dist/src/data/sources/lazy/tabixTsvSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/tabixTsvSource.js +14 -12
- package/dist/src/data/sources/lazy/vcfSource.d.ts +7 -3
- package/dist/src/data/sources/lazy/vcfSource.d.ts.map +1 -1
- package/dist/src/data/sources/lazy/vcfSource.js +7 -8
- package/dist/src/data/sources/urlDescriptor.d.ts +165 -0
- package/dist/src/data/sources/urlDescriptor.d.ts.map +1 -0
- package/dist/src/data/sources/urlDescriptor.js +473 -0
- package/dist/src/data/sources/urlDescriptorController.d.ts +25 -0
- package/dist/src/data/sources/urlDescriptorController.d.ts.map +1 -0
- package/dist/src/data/sources/urlDescriptorController.js +72 -0
- package/dist/src/data/sources/urlDescriptorState.d.ts +47 -0
- package/dist/src/data/sources/urlDescriptorState.d.ts.map +1 -0
- package/dist/src/data/sources/urlDescriptorState.js +129 -0
- package/dist/src/data/sources/urlSource.d.ts.map +1 -1
- package/dist/src/data/sources/urlSource.js +101 -61
- package/dist/src/data/transforms/packLegendLabels.d.ts +21 -0
- package/dist/src/data/transforms/packLegendLabels.d.ts.map +1 -0
- package/dist/src/data/transforms/packLegendLabels.js +189 -0
- package/dist/src/data/transforms/transformFactory.d.ts.map +1 -1
- package/dist/src/data/transforms/transformFactory.js +4 -0
- package/dist/src/data/transforms/truncateText.d.ts +27 -0
- package/dist/src/data/transforms/truncateText.d.ts.map +1 -0
- package/dist/src/data/transforms/truncateText.js +94 -0
- package/dist/src/debug/dataflowDebugSnapshot.d.ts +58 -0
- package/dist/src/debug/dataflowDebugSnapshot.d.ts.map +1 -0
- package/dist/src/debug/dataflowDebugSnapshot.js +159 -0
- package/dist/src/debug/markDebugSnapshot.d.ts +54 -0
- package/dist/src/debug/markDebugSnapshot.d.ts.map +1 -0
- package/dist/src/debug/markDebugSnapshot.js +100 -0
- package/dist/src/debug/paramDebugSnapshot.d.ts +53 -0
- package/dist/src/debug/paramDebugSnapshot.d.ts.map +1 -0
- package/dist/src/debug/paramDebugSnapshot.js +86 -0
- package/dist/src/debug/resolutionDebugSnapshot.d.ts +155 -0
- package/dist/src/debug/resolutionDebugSnapshot.d.ts.map +1 -0
- package/dist/src/debug/resolutionDebugSnapshot.js +291 -0
- package/dist/src/debug/valuePreview.d.ts +9 -0
- package/dist/src/debug/valuePreview.d.ts.map +1 -0
- package/dist/src/debug/valuePreview.js +57 -0
- package/dist/src/debug/viewDebugSnapshot.d.ts +131 -0
- package/dist/src/debug/viewDebugSnapshot.d.ts.map +1 -0
- package/dist/src/debug/viewDebugSnapshot.js +390 -0
- package/dist/src/embedFactory.d.ts.map +1 -1
- package/dist/src/embedFactory.js +6 -1
- package/dist/src/encoder/encoder.d.ts +2 -2
- package/dist/src/encoder/encoder.d.ts.map +1 -1
- package/dist/src/encoder/encoder.js +5 -4
- package/dist/src/fonts/bmFontManager.d.ts +1 -1
- package/dist/src/fonts/bmFontManager.d.ts.map +1 -1
- package/dist/src/fonts/bmFontManager.js +45 -10
- package/dist/src/fonts/textMetrics.d.ts +69 -0
- package/dist/src/fonts/textMetrics.d.ts.map +1 -0
- package/dist/src/fonts/textMetrics.js +73 -0
- package/dist/src/genomeSpy/headlessBootstrap.d.ts.map +1 -1
- package/dist/src/genomeSpy/headlessBootstrap.js +6 -0
- package/dist/src/genomeSpy/renderCoordinator.d.ts.map +1 -1
- package/dist/src/genomeSpy/renderCoordinator.js +25 -3
- package/dist/src/genomeSpy/viewDataInit.d.ts +14 -0
- package/dist/src/genomeSpy/viewDataInit.d.ts.map +1 -1
- package/dist/src/genomeSpy/viewDataInit.js +45 -8
- package/dist/src/genomeSpyBase.d.ts +6 -0
- package/dist/src/genomeSpyBase.d.ts.map +1 -1
- package/dist/src/genomeSpyBase.js +20 -3
- package/dist/src/gl/glslScaleGenerator.d.ts +17 -0
- package/dist/src/gl/glslScaleGenerator.d.ts.map +1 -1
- package/dist/src/gl/glslScaleGenerator.js +39 -2
- package/dist/src/gl/includes/common.glsl.js +1 -1
- package/dist/src/gl/vertexRangeIndex.d.ts.map +1 -1
- package/dist/src/gl/vertexRangeIndex.js +4 -2
- package/dist/src/gl/webGLHelper.d.ts +1 -1
- package/dist/src/gl/webGLHelper.d.ts.map +1 -1
- package/dist/src/gl/webGLHelper.js +13 -8
- package/dist/src/marks/__snapshots__/shaderSnapshot.test.js.snap +140 -3
- package/dist/src/marks/mark.d.ts +47 -4
- package/dist/src/marks/mark.d.ts.map +1 -1
- package/dist/src/marks/mark.js +158 -54
- package/dist/src/marks/point.d.ts.map +1 -1
- package/dist/src/marks/point.js +4 -0
- package/dist/src/marks/point.vertex.glsl.js +1 -1
- package/dist/src/marks/text.d.ts +1 -1
- package/dist/src/marks/text.d.ts.map +1 -1
- package/dist/src/marks/text.js +2 -7
- package/dist/src/marks/text.vertex.glsl.js +1 -1
- package/dist/src/paramRuntime/paramUtils.d.ts +43 -9
- package/dist/src/paramRuntime/paramUtils.d.ts.map +1 -1
- package/dist/src/paramRuntime/paramUtils.js +61 -1
- package/dist/src/paramRuntime/viewParamRuntime.d.ts +32 -0
- package/dist/src/paramRuntime/viewParamRuntime.d.ts.map +1 -1
- package/dist/src/paramRuntime/viewParamRuntime.js +63 -0
- package/dist/src/scales/axisResolution.d.ts +35 -0
- package/dist/src/scales/axisResolution.d.ts.map +1 -1
- package/dist/src/scales/axisResolution.js +115 -7
- package/dist/src/scales/legendResolution.d.ts +83 -0
- package/dist/src/scales/legendResolution.d.ts.map +1 -0
- package/dist/src/scales/legendResolution.js +461 -0
- package/dist/src/scales/scaleResolution.d.ts +36 -0
- package/dist/src/scales/scaleResolution.d.ts.map +1 -1
- package/dist/src/scales/scaleResolution.js +59 -0
- package/dist/src/scales/viewLevelGuideConfig.d.ts +53 -0
- package/dist/src/scales/viewLevelGuideConfig.d.ts.map +1 -0
- package/dist/src/scales/viewLevelGuideConfig.js +224 -0
- package/dist/src/scales/viewLevelScaleConfig.d.ts.map +1 -1
- package/dist/src/scales/viewLevelScaleConfig.js +13 -2
- package/dist/src/spec/axis.d.ts +109 -3
- package/dist/src/spec/channel.d.ts +23 -4
- package/dist/src/spec/config.d.ts +59 -4
- package/dist/src/spec/data.d.ts +177 -17
- package/dist/src/spec/legend.d.ts +246 -0
- package/dist/src/spec/mark.d.ts +16 -4
- package/dist/src/spec/title.d.ts +58 -1
- package/dist/src/spec/transform.d.ts +149 -0
- package/dist/src/spec/view.d.ts +39 -6
- package/dist/src/types/embedApi.d.ts +262 -6
- package/dist/src/types/rendering.d.ts +19 -3
- package/dist/src/types/viewContext.d.ts +18 -2
- package/dist/src/utils/arrayUtils.d.ts +11 -0
- package/dist/src/utils/arrayUtils.d.ts.map +1 -1
- package/dist/src/utils/arrayUtils.js +23 -0
- package/dist/src/utils/suspension.d.ts +17 -0
- package/dist/src/utils/suspension.d.ts.map +1 -0
- package/dist/src/utils/suspension.js +41 -0
- package/dist/src/view/axisGridView.d.ts.map +1 -1
- package/dist/src/view/axisGridView.js +1 -4
- package/dist/src/view/axisView.d.ts +18 -2
- package/dist/src/view/axisView.d.ts.map +1 -1
- package/dist/src/view/axisView.js +180 -75
- package/dist/src/view/concatView.d.ts +10 -2
- package/dist/src/view/concatView.d.ts.map +1 -1
- package/dist/src/view/concatView.js +46 -9
- package/dist/src/view/containerMutationHelper.d.ts +20 -1
- package/dist/src/view/containerMutationHelper.d.ts.map +1 -1
- package/dist/src/view/containerMutationHelper.js +196 -33
- package/dist/src/view/facetView.d.ts +1 -1
- package/dist/src/view/gridView/gridChild.d.ts +54 -4
- package/dist/src/view/gridView/gridChild.d.ts.map +1 -1
- package/dist/src/view/gridView/gridChild.js +301 -120
- package/dist/src/view/gridView/gridChildLegends.d.ts +57 -0
- package/dist/src/view/gridView/gridChildLegends.d.ts.map +1 -0
- package/dist/src/view/gridView/gridChildLegends.js +503 -0
- package/dist/src/view/gridView/gridView.d.ts +25 -0
- package/dist/src/view/gridView/gridView.d.ts.map +1 -1
- package/dist/src/view/gridView/gridView.js +490 -78
- package/dist/src/view/gridView/legendLayout.d.ts +30 -0
- package/dist/src/view/gridView/legendLayout.d.ts.map +1 -0
- package/dist/src/view/gridView/legendLayout.js +115 -0
- package/dist/src/view/gridView/scrollbar.d.ts.map +1 -1
- package/dist/src/view/gridView/scrollbar.js +1 -4
- package/dist/src/view/gridView/selectionRect.d.ts.map +1 -1
- package/dist/src/view/gridView/selectionRect.js +1 -4
- package/dist/src/view/gridView/separatorView.d.ts.map +1 -1
- package/dist/src/view/gridView/separatorView.js +1 -4
- package/dist/src/view/layerView.d.ts +9 -2
- package/dist/src/view/layerView.d.ts.map +1 -1
- package/dist/src/view/layerView.js +18 -1
- package/dist/src/view/layout/flexLayout.d.ts +20 -4
- package/dist/src/view/layout/flexLayout.d.ts.map +1 -1
- package/dist/src/view/layout/flexLayout.js +331 -31
- package/dist/src/view/layout/rectangle.d.ts +14 -0
- package/dist/src/view/layout/rectangle.d.ts.map +1 -1
- package/dist/src/view/layout/rectangle.js +40 -0
- package/dist/src/view/legend/legendEntries.d.ts +20 -0
- package/dist/src/view/legend/legendEntries.d.ts.map +1 -0
- package/dist/src/view/legend/legendEntries.js +21 -0
- package/dist/src/view/legendView.d.ts +137 -0
- package/dist/src/view/legendView.d.ts.map +1 -0
- package/dist/src/view/legendView.js +1654 -0
- package/dist/src/view/renderingContext/bufferedViewRenderingContext.d.ts.map +1 -1
- package/dist/src/view/renderingContext/bufferedViewRenderingContext.js +26 -4
- package/dist/src/view/renderingContext/clipOptions.d.ts +44 -0
- package/dist/src/view/renderingContext/clipOptions.d.ts.map +1 -0
- package/dist/src/view/renderingContext/clipOptions.js +140 -0
- package/dist/src/view/renderingContext/simpleViewRenderingContext.d.ts.map +1 -1
- package/dist/src/view/renderingContext/simpleViewRenderingContext.js +12 -1
- package/dist/src/view/resolutionPlanner.d.ts +2 -1
- package/dist/src/view/resolutionPlanner.d.ts.map +1 -1
- package/dist/src/view/resolutionPlanner.js +89 -25
- package/dist/src/view/testUtils.d.ts +4 -2
- package/dist/src/view/testUtils.d.ts.map +1 -1
- package/dist/src/view/testUtils.js +60 -7
- package/dist/src/view/titleView.d.ts +37 -0
- package/dist/src/view/titleView.d.ts.map +1 -0
- package/dist/src/view/titleView.js +584 -0
- package/dist/src/view/unitView.d.ts +3 -3
- package/dist/src/view/unitView.d.ts.map +1 -1
- package/dist/src/view/unitView.js +3 -2
- package/dist/src/view/view.d.ts +25 -24
- package/dist/src/view/view.d.ts.map +1 -1
- package/dist/src/view/view.js +126 -16
- package/dist/src/view/viewChrome.d.ts +33 -0
- package/dist/src/view/viewChrome.d.ts.map +1 -0
- package/dist/src/view/viewChrome.js +64 -0
- package/dist/src/view/viewFactory.d.ts +2 -5
- package/dist/src/view/viewFactory.d.ts.map +1 -1
- package/dist/src/view/viewFactory.js +1 -2
- package/dist/src/view/viewIdentityRegistry.d.ts +37 -0
- package/dist/src/view/viewIdentityRegistry.d.ts.map +1 -0
- package/dist/src/view/viewIdentityRegistry.js +71 -0
- package/dist/src/view/viewMutationAcidTestUtils.d.ts +112 -0
- package/dist/src/view/viewMutationAcidTestUtils.d.ts.map +1 -0
- package/dist/src/view/viewMutationAcidTestUtils.js +234 -0
- package/dist/src/view/viewMutationApi.d.ts +42 -0
- package/dist/src/view/viewMutationApi.d.ts.map +1 -0
- package/dist/src/view/viewMutationApi.js +811 -0
- package/dist/src/view/viewSelectors.d.ts +11 -9
- package/dist/src/view/viewSelectors.d.ts.map +1 -1
- package/dist/src/view/viewSelectors.js +28 -17
- package/package.json +4 -4
- package/dist/bundle/esm-CuMSzCHy.js +0 -298
- package/dist/bundle/esm-DAnOffpD.js +0 -1426
- package/dist/bundle/esm-DMXpJXM4.js +0 -369
- package/dist/bundle/esm-DNtC3H80.js +0 -121
- package/dist/src/view/title.d.ts +0 -13
- package/dist/src/view/title.d.ts.map +0 -1
- package/dist/src/view/title.js +0 -154
- /package/dist/bundle/{AbortablePromiseCache-3gHJdF3E.js → AbortablePromiseCache-BTmAcN-t.js} +0 -0
- /package/dist/bundle/{esm-CuVa5T98.js → esm-VvpZ9hsq.js} +0 -0
- /package/dist/bundle/{chunk-DmhlhrBa.js → rolldown-runtime-Dy4uBu1J.js} +0 -0
|
@@ -0,0 +1,1654 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Legend generation is largely modeled after Vega and Vega-Lite legends,
|
|
3
|
+
* including the public properties, default values, and legend-type/entry
|
|
4
|
+
* logic. The most relevant references are:
|
|
5
|
+
*
|
|
6
|
+
* - vega-lite/src/legend.ts
|
|
7
|
+
* - vega-lite/src/compile/legend/*
|
|
8
|
+
* - vega/packages/vega-parser/src/parsers/legend.js
|
|
9
|
+
* - vega/packages/vega-parser/src/parsers/guides/legend-*.js
|
|
10
|
+
* - vega/packages/vega-view-transforms/src/layout/legend.js
|
|
11
|
+
*
|
|
12
|
+
* GenomeSpy's implementation is intentionally different. Vega emits a Vega
|
|
13
|
+
* legend definition and relies on the scenegraph plus legend layout transform
|
|
14
|
+
* for final mark bounds and placement. GenomeSpy instead builds legends from
|
|
15
|
+
* ordinary internal views and marks, uses `measureText`/`packLegendLabels` for
|
|
16
|
+
* symbol entry layout, and lets `GridChild` own local side/corner placement and
|
|
17
|
+
* stacked legend regions.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import ContainerView from "./containerView.js";
|
|
21
|
+
import {
|
|
22
|
+
FlexDimensions,
|
|
23
|
+
getLargestSize,
|
|
24
|
+
getSizeDefMaxPx,
|
|
25
|
+
getSizeDefMinPx,
|
|
26
|
+
mapToPixelCoords,
|
|
27
|
+
sumSizeDefs,
|
|
28
|
+
} from "./layout/flexLayout.js";
|
|
29
|
+
import Rectangle from "./layout/rectangle.js";
|
|
30
|
+
import UnitView from "./unitView.js";
|
|
31
|
+
import { markViewAsChrome, markViewAsNonAddressable } from "./viewSelectors.js";
|
|
32
|
+
import { truncateText } from "../data/transforms/truncateText.js";
|
|
33
|
+
import { measureText, requestFont } from "../fonts/textMetrics.js";
|
|
34
|
+
|
|
35
|
+
const LABEL_WIDTH_FIELD = "_legendLabelWidth";
|
|
36
|
+
const SYMBOL_SIZE_FIELD = "_legendSymbolSize";
|
|
37
|
+
const SYMBOL_STROKE_WIDTH_FIELD = "_legendStrokeWidth";
|
|
38
|
+
const DEFAULT_GRADIENT_LEGEND_LENGTH = 200;
|
|
39
|
+
const DEFAULT_GRADIENT_SAMPLE_COUNT = 64;
|
|
40
|
+
const DEFAULT_GRADIENT_TICK_COUNT = 5;
|
|
41
|
+
const DEFAULT_GRADIENT_THICKNESS = 12;
|
|
42
|
+
const DEFAULT_GRADIENT_TICK_SIZE = 4;
|
|
43
|
+
const MIN_GRADIENT_LEGEND_LENGTH = 40;
|
|
44
|
+
const AUTO_EXTENT_GROW_THRESHOLD_PX = 2;
|
|
45
|
+
/** @type {import("../spec/view.js").ViewBackground} */
|
|
46
|
+
const LEGEND_VIEW_BACKGROUND = {
|
|
47
|
+
fillOpacity: 0,
|
|
48
|
+
shadowOpacity: 0,
|
|
49
|
+
strokeOpacity: 0,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Legend internals use scale-backed helper marks but must not create their own
|
|
54
|
+
* axes or legends from inherited configuration.
|
|
55
|
+
*
|
|
56
|
+
* This must be applied to every generated child spec, not just the LegendView
|
|
57
|
+
* root. The generated subtree contains ordinary unit views, including text
|
|
58
|
+
* views and helper marks with internal x/y placement channels. Those channels
|
|
59
|
+
* are implementation details of the legend, even when they are value-backed and
|
|
60
|
+
* not semantically scale-backed data encodings.
|
|
61
|
+
*
|
|
62
|
+
* Without this exclusion, a generated child can still end up with an axis
|
|
63
|
+
* resolution and inherit axis defaults such as `grid: true`, creating an
|
|
64
|
+
* AxisGridView inside the legend. This has been observed with the title view:
|
|
65
|
+
* the authored title encoding only had `text`, but the unexcluded generated
|
|
66
|
+
* title still received an x-axis resolution through the normal guide machinery.
|
|
67
|
+
*
|
|
68
|
+
* The legend may still force the represented source scale, e.g. color/size, but
|
|
69
|
+
* generated helper x/y channels and nested legend channels must stay out of the
|
|
70
|
+
* normal guide resolution machinery.
|
|
71
|
+
*
|
|
72
|
+
* See:
|
|
73
|
+
* - https://github.com/genome-spy/genome-spy/issues/412
|
|
74
|
+
* - https://github.com/genome-spy/genome-spy/issues/413
|
|
75
|
+
*
|
|
76
|
+
* @template {import("../spec/view.js").ViewSpec & {
|
|
77
|
+
* resolve?: any,
|
|
78
|
+
* layer?: any[],
|
|
79
|
+
* vconcat?: any[],
|
|
80
|
+
* hconcat?: any[]
|
|
81
|
+
* }} T
|
|
82
|
+
* @param {T} spec
|
|
83
|
+
* @returns {T}
|
|
84
|
+
*/
|
|
85
|
+
function excludeLegendGuideResolutions(spec) {
|
|
86
|
+
spec.resolve = {
|
|
87
|
+
...spec.resolve,
|
|
88
|
+
axis: { default: "excluded", ...spec.resolve?.axis },
|
|
89
|
+
legend: { default: "excluded", ...spec.resolve?.legend },
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
for (const children of [spec.layer, spec.vconcat, spec.hconcat]) {
|
|
93
|
+
children?.forEach(excludeLegendGuideResolutions);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return spec;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @typedef {import("../spec/legend.js").LegendConfig} LegendConfig
|
|
101
|
+
* @typedef {import("./legend/legendEntries.js").LegendEntry} LegendEntry
|
|
102
|
+
* @typedef {(import("../spec/view.js").VConcatSpec | import("../spec/view.js").HConcatSpec) & {
|
|
103
|
+
* view: import("../spec/view.js").ViewBackground
|
|
104
|
+
* }} LegendRootSpec
|
|
105
|
+
* @typedef {{
|
|
106
|
+
* mark?: Partial<import("../spec/mark.js").PointProps>,
|
|
107
|
+
* encoding?: Partial<import("../spec/channel.js").Encoding>
|
|
108
|
+
* }} SymbolLegendStyle
|
|
109
|
+
*/
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @param {LegendConfig} legend
|
|
113
|
+
* @returns {import("../spec/view.js").ViewBackground}
|
|
114
|
+
*/
|
|
115
|
+
function createLegendViewBackground(legend) {
|
|
116
|
+
return {
|
|
117
|
+
fill: legend.backgroundFill,
|
|
118
|
+
fillOpacity: legend.backgroundFill
|
|
119
|
+
? (legend.backgroundFillOpacity ?? 1)
|
|
120
|
+
: 0,
|
|
121
|
+
stroke: legend.backgroundStroke,
|
|
122
|
+
strokeWidth: legend.backgroundStrokeWidth,
|
|
123
|
+
strokeOpacity: legend.backgroundStroke
|
|
124
|
+
? (legend.backgroundStrokeOpacity ?? 1)
|
|
125
|
+
: 0,
|
|
126
|
+
shadowOpacity: 0,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {LegendConfig} legend
|
|
132
|
+
* @param {import("../types/viewContext.js").default} context
|
|
133
|
+
* @returns {import("../spec/view.js").UnitSpec | undefined}
|
|
134
|
+
*/
|
|
135
|
+
function createLegendTitleSpec(legend, context) {
|
|
136
|
+
const title = legend.title;
|
|
137
|
+
|
|
138
|
+
if (!title) {
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const titleFontSize = legend.titleFontSize ?? 11;
|
|
143
|
+
const titlePadding = legend.titlePadding ?? 5;
|
|
144
|
+
const sideTitle = isSideTitle(legend);
|
|
145
|
+
const titleWidth = Math.ceil(getTitleWidth(legend, context));
|
|
146
|
+
const sideTitleWidth = titleWidth + titlePadding;
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
name: "title",
|
|
150
|
+
width: sideTitle ? sideTitleWidth : undefined,
|
|
151
|
+
height: sideTitle ? { grow: 1 } : titleFontSize + titlePadding,
|
|
152
|
+
view: LEGEND_VIEW_BACKGROUND,
|
|
153
|
+
data: {
|
|
154
|
+
values: [{ label: title }],
|
|
155
|
+
},
|
|
156
|
+
transform: [
|
|
157
|
+
{
|
|
158
|
+
type: "truncateText",
|
|
159
|
+
field: "label",
|
|
160
|
+
limit: legend.titleLimit,
|
|
161
|
+
fontSize: titleFontSize,
|
|
162
|
+
font: legend.titleFont,
|
|
163
|
+
fontStyle: legend.titleFontStyle,
|
|
164
|
+
fontWeight: legend.titleFontWeight,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
mark: {
|
|
168
|
+
type: "text",
|
|
169
|
+
clip: false,
|
|
170
|
+
x:
|
|
171
|
+
getTitleOrient(legend) == "right" && sideTitleWidth > 0
|
|
172
|
+
? titlePadding / sideTitleWidth
|
|
173
|
+
: 0,
|
|
174
|
+
y: 0.5,
|
|
175
|
+
align: "left",
|
|
176
|
+
baseline: "middle",
|
|
177
|
+
color: legend.titleColor,
|
|
178
|
+
font: legend.titleFont,
|
|
179
|
+
fontStyle: legend.titleFontStyle,
|
|
180
|
+
fontWeight: legend.titleFontWeight,
|
|
181
|
+
size: titleFontSize,
|
|
182
|
+
},
|
|
183
|
+
encoding: {
|
|
184
|
+
text: { field: "label" },
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* @param {LegendConfig} legend
|
|
191
|
+
* @param {import("../spec/view.js").ViewSpec} body
|
|
192
|
+
* @param {import("../types/viewContext.js").default} context
|
|
193
|
+
* @param {import("../spec/channel.js").ChannelWithScale[]} [forcedScaleChannels]
|
|
194
|
+
* @returns {LegendRootSpec}
|
|
195
|
+
*/
|
|
196
|
+
function createLegendRootSpec(legend, body, context, forcedScaleChannels = []) {
|
|
197
|
+
const title = createLegendTitleSpec(legend, context);
|
|
198
|
+
/** @type {{ vconcat: import("../spec/view.js").ViewSpec[] } | { hconcat: import("../spec/view.js").ViewSpec[] }} */
|
|
199
|
+
let children;
|
|
200
|
+
|
|
201
|
+
if (!title) {
|
|
202
|
+
children = { vconcat: [body] };
|
|
203
|
+
} else if (getTitleOrient(legend) == "bottom") {
|
|
204
|
+
children = { vconcat: [body, title] };
|
|
205
|
+
} else if (getTitleOrient(legend) == "left") {
|
|
206
|
+
children = { hconcat: [title, body] };
|
|
207
|
+
} else if (getTitleOrient(legend) == "right") {
|
|
208
|
+
children = { hconcat: [body, title] };
|
|
209
|
+
} else {
|
|
210
|
+
children = { vconcat: [title, body] };
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return excludeLegendGuideResolutions({
|
|
214
|
+
name: "legend_" + (legend.orient ?? "right"),
|
|
215
|
+
padding: legend.padding,
|
|
216
|
+
view: createLegendViewBackground(legend),
|
|
217
|
+
resolve: {
|
|
218
|
+
scale: Object.fromEntries(
|
|
219
|
+
forcedScaleChannels.map((channel) => [channel, "forced"])
|
|
220
|
+
),
|
|
221
|
+
},
|
|
222
|
+
spacing: 0,
|
|
223
|
+
...children,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {LegendConfig} legend
|
|
229
|
+
*/
|
|
230
|
+
function isTopBottomLegend(legend) {
|
|
231
|
+
return legend.orient == "top" || legend.orient == "bottom";
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* @param {LegendConfig} legend
|
|
236
|
+
*/
|
|
237
|
+
function isHorizontalLegend(legend) {
|
|
238
|
+
return isTopBottomLegend(legend) || legend.direction == "horizontal";
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* @param {LegendConfig} legend
|
|
243
|
+
* @returns {import("../spec/legend.js").LegendTitleOrient}
|
|
244
|
+
*/
|
|
245
|
+
function getTitleOrient(legend) {
|
|
246
|
+
return legend.titleOrient ?? "top";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* @param {LegendConfig} legend
|
|
251
|
+
*/
|
|
252
|
+
function isSideTitle(legend) {
|
|
253
|
+
const titleOrient = getTitleOrient(legend);
|
|
254
|
+
|
|
255
|
+
return titleOrient == "left" || titleOrient == "right";
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @param {string | undefined} value
|
|
260
|
+
* @returns {import("../spec/channel.js").ValueDef | undefined}
|
|
261
|
+
*/
|
|
262
|
+
function createBaseColorEncoding(value) {
|
|
263
|
+
if (value === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
} else if (value == "transparent") {
|
|
266
|
+
return { value: null };
|
|
267
|
+
} else {
|
|
268
|
+
return { value };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Rule and link marks use the `size` channel as stroke width. Vega/Vega-Lite
|
|
274
|
+
* still represent comparable guides as symbol legends; GenomeSpy renders this
|
|
275
|
+
* variant as a short rule so the legend explains stroke width directly.
|
|
276
|
+
*
|
|
277
|
+
* @param {object} options
|
|
278
|
+
* @param {import("../spec/channel.js").ChannelWithScale} options.channel
|
|
279
|
+
* @param {import("../spec/channel.js").Type} options.dataType
|
|
280
|
+
* @param {import("../spec/scale.js").Scale} options.horizontalPixelScale
|
|
281
|
+
* @param {import("../spec/scale.js").Scale} options.verticalPixelScale
|
|
282
|
+
* @param {LegendConfig} options.legend
|
|
283
|
+
* @param {SymbolLegendStyle} options.symbolStyle
|
|
284
|
+
* @returns {import("../spec/view.js").UnitSpec}
|
|
285
|
+
*/
|
|
286
|
+
function createStrokeSymbolLayer({
|
|
287
|
+
channel,
|
|
288
|
+
dataType,
|
|
289
|
+
horizontalPixelScale,
|
|
290
|
+
verticalPixelScale,
|
|
291
|
+
legend,
|
|
292
|
+
symbolStyle,
|
|
293
|
+
}) {
|
|
294
|
+
const styleEncoding =
|
|
295
|
+
/** @type {Partial<import("../spec/channel.js").Encoding>} */ (
|
|
296
|
+
symbolStyle.encoding ?? {}
|
|
297
|
+
);
|
|
298
|
+
const color =
|
|
299
|
+
styleEncoding.color ??
|
|
300
|
+
styleEncoding.stroke ??
|
|
301
|
+
styleEncoding.fill ??
|
|
302
|
+
createBaseColorEncoding(legend.symbolBaseStrokeColor);
|
|
303
|
+
|
|
304
|
+
return {
|
|
305
|
+
name: "symbols",
|
|
306
|
+
mark: {
|
|
307
|
+
type: "rule",
|
|
308
|
+
clip: false,
|
|
309
|
+
cullByVisibleRange: false,
|
|
310
|
+
opacity: symbolStyle.mark?.opacity,
|
|
311
|
+
},
|
|
312
|
+
encoding: {
|
|
313
|
+
x: {
|
|
314
|
+
field: "symbolX",
|
|
315
|
+
type: "quantitative",
|
|
316
|
+
scale: horizontalPixelScale,
|
|
317
|
+
axis: null,
|
|
318
|
+
buildIndex: false,
|
|
319
|
+
},
|
|
320
|
+
x2: {
|
|
321
|
+
field: "symbolX2",
|
|
322
|
+
type: "quantitative",
|
|
323
|
+
scale: horizontalPixelScale,
|
|
324
|
+
axis: null,
|
|
325
|
+
},
|
|
326
|
+
y: {
|
|
327
|
+
field: "labelY2",
|
|
328
|
+
type: "quantitative",
|
|
329
|
+
scale: verticalPixelScale,
|
|
330
|
+
axis: null,
|
|
331
|
+
},
|
|
332
|
+
y2: {
|
|
333
|
+
field: "labelY2",
|
|
334
|
+
type: "quantitative",
|
|
335
|
+
scale: verticalPixelScale,
|
|
336
|
+
axis: null,
|
|
337
|
+
},
|
|
338
|
+
[channel]: {
|
|
339
|
+
field: "value",
|
|
340
|
+
type: dataType,
|
|
341
|
+
domainInert: true,
|
|
342
|
+
},
|
|
343
|
+
...(color ? { color } : {}),
|
|
344
|
+
...(styleEncoding.opacity
|
|
345
|
+
? { opacity: styleEncoding.opacity }
|
|
346
|
+
: {}),
|
|
347
|
+
},
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @param {object} options
|
|
353
|
+
* @param {LegendEntry[]} [options.entries]
|
|
354
|
+
* @param {import("../spec/channel.js").ChannelWithScale} options.channel
|
|
355
|
+
* @param {Partial<Record<import("../spec/channel.js").ChannelWithScale, string>>} [options.symbolChannels]
|
|
356
|
+
* @param {SymbolLegendStyle} [options.symbolStyle]
|
|
357
|
+
* @param {"point" | "stroke"} [options.symbolGeometry]
|
|
358
|
+
* @param {LegendConfig} options.legend
|
|
359
|
+
* @param {string} [options.format]
|
|
360
|
+
* @param {import("../spec/channel.js").Type} options.dataType
|
|
361
|
+
* @param {import("../types/viewContext.js").default} options.context
|
|
362
|
+
* @returns {LegendRootSpec}
|
|
363
|
+
*/
|
|
364
|
+
export function createSymbolLegendSpec({
|
|
365
|
+
entries,
|
|
366
|
+
channel,
|
|
367
|
+
symbolChannels = {},
|
|
368
|
+
symbolStyle = {},
|
|
369
|
+
symbolGeometry = "point",
|
|
370
|
+
legend,
|
|
371
|
+
format,
|
|
372
|
+
dataType,
|
|
373
|
+
context,
|
|
374
|
+
}) {
|
|
375
|
+
const strokeSymbol = symbolGeometry == "stroke";
|
|
376
|
+
const horizontalLegend = isHorizontalLegend(legend);
|
|
377
|
+
const labelAlign = legend.labelAlign ?? "left";
|
|
378
|
+
const labelBaseline = legend.labelBaseline ?? "middle";
|
|
379
|
+
const labelFontSize = legend.labelFontSize ?? 10;
|
|
380
|
+
const scaledSymbolChannels = new Set([
|
|
381
|
+
channel,
|
|
382
|
+
...Object.keys(symbolChannels),
|
|
383
|
+
]);
|
|
384
|
+
const isBaseColorChannelScaled = (
|
|
385
|
+
/** @type {import("../spec/channel.js").ChannelWithScale} */ channel
|
|
386
|
+
) => scaledSymbolChannels.has(channel) || scaledSymbolChannels.has("color");
|
|
387
|
+
const baseSymbolEncoding = Object.fromEntries(
|
|
388
|
+
/** @type {const} */ ([
|
|
389
|
+
["fill", createBaseColorEncoding(legend.symbolBaseFillColor)],
|
|
390
|
+
["stroke", createBaseColorEncoding(legend.symbolBaseStrokeColor)],
|
|
391
|
+
])
|
|
392
|
+
.filter(
|
|
393
|
+
([channel, encoding]) =>
|
|
394
|
+
!isBaseColorChannelScaled(channel) && encoding !== undefined
|
|
395
|
+
)
|
|
396
|
+
.map(([channel, encoding]) => [channel, encoding])
|
|
397
|
+
);
|
|
398
|
+
const horizontalPixelScale = {
|
|
399
|
+
domain: [0, { expr: "width" }],
|
|
400
|
+
zero: false,
|
|
401
|
+
nice: false,
|
|
402
|
+
};
|
|
403
|
+
const verticalPixelScale = {
|
|
404
|
+
domain: [0, { expr: "height" }],
|
|
405
|
+
zero: false,
|
|
406
|
+
nice: false,
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
/** @type {import("../spec/view.js").UnitSpec[]} */
|
|
410
|
+
const layer = [
|
|
411
|
+
strokeSymbol
|
|
412
|
+
? createStrokeSymbolLayer({
|
|
413
|
+
channel,
|
|
414
|
+
dataType,
|
|
415
|
+
horizontalPixelScale,
|
|
416
|
+
verticalPixelScale,
|
|
417
|
+
legend,
|
|
418
|
+
symbolStyle,
|
|
419
|
+
})
|
|
420
|
+
: {
|
|
421
|
+
name: "symbols",
|
|
422
|
+
mark: {
|
|
423
|
+
type: "point",
|
|
424
|
+
clip: false,
|
|
425
|
+
cullByVisibleRange: false,
|
|
426
|
+
filled: channel == "fill",
|
|
427
|
+
shape: legend.symbolType,
|
|
428
|
+
size: legend.symbolSize,
|
|
429
|
+
strokeWidth: legend.symbolStrokeWidth,
|
|
430
|
+
...symbolStyle.mark,
|
|
431
|
+
},
|
|
432
|
+
encoding: {
|
|
433
|
+
x: {
|
|
434
|
+
field: "entryX",
|
|
435
|
+
type: "quantitative",
|
|
436
|
+
scale: horizontalPixelScale,
|
|
437
|
+
axis: null,
|
|
438
|
+
buildIndex: false,
|
|
439
|
+
},
|
|
440
|
+
y: {
|
|
441
|
+
field: "labelY2",
|
|
442
|
+
type: "quantitative",
|
|
443
|
+
scale: verticalPixelScale,
|
|
444
|
+
axis: null,
|
|
445
|
+
},
|
|
446
|
+
[channel]: {
|
|
447
|
+
field: "value",
|
|
448
|
+
type: dataType,
|
|
449
|
+
domainInert: true,
|
|
450
|
+
},
|
|
451
|
+
...baseSymbolEncoding,
|
|
452
|
+
...symbolStyle.encoding,
|
|
453
|
+
...Object.fromEntries(
|
|
454
|
+
Object.entries(symbolChannels).map(([channel]) => [
|
|
455
|
+
channel,
|
|
456
|
+
{
|
|
457
|
+
field: "value",
|
|
458
|
+
type: dataType,
|
|
459
|
+
domainInert: true,
|
|
460
|
+
},
|
|
461
|
+
])
|
|
462
|
+
),
|
|
463
|
+
},
|
|
464
|
+
},
|
|
465
|
+
];
|
|
466
|
+
|
|
467
|
+
layer.push({
|
|
468
|
+
name: "labels",
|
|
469
|
+
mark: {
|
|
470
|
+
type: "text",
|
|
471
|
+
clip: false,
|
|
472
|
+
cullByVisibleRange: false,
|
|
473
|
+
align: labelAlign,
|
|
474
|
+
baseline: labelBaseline,
|
|
475
|
+
color: legend.labelColor,
|
|
476
|
+
font: legend.labelFont,
|
|
477
|
+
fontStyle: legend.labelFontStyle,
|
|
478
|
+
fontWeight: legend.labelFontWeight,
|
|
479
|
+
size: labelFontSize,
|
|
480
|
+
},
|
|
481
|
+
encoding: {
|
|
482
|
+
x: {
|
|
483
|
+
field: "labelX",
|
|
484
|
+
type: "quantitative",
|
|
485
|
+
scale: horizontalPixelScale,
|
|
486
|
+
axis: null,
|
|
487
|
+
buildIndex: false,
|
|
488
|
+
},
|
|
489
|
+
y: {
|
|
490
|
+
field: "labelY2",
|
|
491
|
+
type: "quantitative",
|
|
492
|
+
scale: verticalPixelScale,
|
|
493
|
+
axis: null,
|
|
494
|
+
},
|
|
495
|
+
text: { field: "label" },
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
return createLegendRootSpec(
|
|
500
|
+
legend,
|
|
501
|
+
{
|
|
502
|
+
name: "legendBody",
|
|
503
|
+
height: { grow: 1 },
|
|
504
|
+
view: LEGEND_VIEW_BACKGROUND,
|
|
505
|
+
resolve: {
|
|
506
|
+
scale: { x: "excluded", y: "excluded" },
|
|
507
|
+
axis: { x: "excluded", y: "excluded" },
|
|
508
|
+
},
|
|
509
|
+
data: entries
|
|
510
|
+
? { values: entries }
|
|
511
|
+
: {
|
|
512
|
+
lazy: {
|
|
513
|
+
type: "legendEntries",
|
|
514
|
+
channel,
|
|
515
|
+
dataType,
|
|
516
|
+
format,
|
|
517
|
+
values: legend.values,
|
|
518
|
+
sizeMode: strokeSymbol ? "strokeWidth" : "area",
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
transform: [
|
|
522
|
+
{
|
|
523
|
+
type: "truncateText",
|
|
524
|
+
field: "label",
|
|
525
|
+
limit: legend.labelLimit,
|
|
526
|
+
fontSize: labelFontSize,
|
|
527
|
+
font: legend.labelFont,
|
|
528
|
+
fontStyle: legend.labelFontStyle,
|
|
529
|
+
fontWeight: legend.labelFontWeight,
|
|
530
|
+
},
|
|
531
|
+
{
|
|
532
|
+
type: "measureText",
|
|
533
|
+
field: "label",
|
|
534
|
+
as: LABEL_WIDTH_FIELD,
|
|
535
|
+
fontSize: labelFontSize,
|
|
536
|
+
font: legend.labelFont,
|
|
537
|
+
fontStyle: legend.labelFontStyle,
|
|
538
|
+
fontWeight: legend.labelFontWeight,
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
type: "packLegendLabels",
|
|
542
|
+
labelWidth: LABEL_WIDTH_FIELD,
|
|
543
|
+
columns: legend.columns,
|
|
544
|
+
symbolSize: strokeSymbol
|
|
545
|
+
? legend.symbolSize
|
|
546
|
+
: channel == "size"
|
|
547
|
+
? SYMBOL_SIZE_FIELD
|
|
548
|
+
: legend.symbolSize,
|
|
549
|
+
symbolStrokeWidth: strokeSymbol
|
|
550
|
+
? SYMBOL_STROKE_WIDTH_FIELD
|
|
551
|
+
: legend.symbolStrokeWidth,
|
|
552
|
+
labelOffset: legend.labelOffset,
|
|
553
|
+
fontSize: labelFontSize,
|
|
554
|
+
rowPadding: legend.rowPadding,
|
|
555
|
+
columnPadding: legend.columnPadding,
|
|
556
|
+
symbolOffset: legend.symbolOffset,
|
|
557
|
+
yOffset: 0,
|
|
558
|
+
yExtent: { expr: "height" },
|
|
559
|
+
direction: horizontalLegend
|
|
560
|
+
? "horizontal"
|
|
561
|
+
: (legend.direction ?? "vertical"),
|
|
562
|
+
},
|
|
563
|
+
],
|
|
564
|
+
layer,
|
|
565
|
+
},
|
|
566
|
+
context,
|
|
567
|
+
[channel, ...Object.keys(symbolChannels)]
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* @param {object} options
|
|
573
|
+
* @param {import("../spec/channel.js").ChannelWithScale} options.channel
|
|
574
|
+
* @param {LegendConfig} options.legend
|
|
575
|
+
* @param {string} [options.format]
|
|
576
|
+
* @param {import("../types/viewContext.js").default} options.context
|
|
577
|
+
* @returns {LegendRootSpec}
|
|
578
|
+
*/
|
|
579
|
+
export function createGradientLegendSpec({ channel, legend, format, context }) {
|
|
580
|
+
const h = isHorizontalLegend(legend);
|
|
581
|
+
const labelAlign = h ? "center" : (legend.labelAlign ?? "left");
|
|
582
|
+
const labelBaseline = h ? "top" : (legend.labelBaseline ?? "middle");
|
|
583
|
+
const labelFontSize = legend.labelFontSize ?? 10;
|
|
584
|
+
const labelOffset = legend.labelOffset ?? 4;
|
|
585
|
+
const xs = {
|
|
586
|
+
domain: [0, { expr: "width" }],
|
|
587
|
+
zero: false,
|
|
588
|
+
nice: false,
|
|
589
|
+
};
|
|
590
|
+
const ys = {
|
|
591
|
+
domain: [0, { expr: "height" }],
|
|
592
|
+
zero: false,
|
|
593
|
+
nice: false,
|
|
594
|
+
};
|
|
595
|
+
const ps = {
|
|
596
|
+
domain: [0, 1],
|
|
597
|
+
domainTransition: false,
|
|
598
|
+
zero: false,
|
|
599
|
+
nice: false,
|
|
600
|
+
};
|
|
601
|
+
const labelY = labelFontSize;
|
|
602
|
+
const tickY = labelY + labelOffset;
|
|
603
|
+
const tickY2 = tickY + DEFAULT_GRADIENT_TICK_SIZE;
|
|
604
|
+
const gradientY = tickY2;
|
|
605
|
+
const gradientY2 = gradientY + DEFAULT_GRADIENT_THICKNESS;
|
|
606
|
+
const tickX = DEFAULT_GRADIENT_THICKNESS;
|
|
607
|
+
const tickX2 = DEFAULT_GRADIENT_THICKNESS + DEFAULT_GRADIENT_TICK_SIZE;
|
|
608
|
+
const labelX = tickX2 + labelOffset;
|
|
609
|
+
// p is the gradient axis; q is the cross axis.
|
|
610
|
+
const p = h ? "x" : "y";
|
|
611
|
+
const p2 = /** @type {"x2" | "y2"} */ (p + "2");
|
|
612
|
+
const q = h ? "y" : "x";
|
|
613
|
+
const q2 = /** @type {"x2" | "y2"} */ (q + "2");
|
|
614
|
+
const qs = h ? ys : xs;
|
|
615
|
+
const band0 = "_legendGradientBandStart";
|
|
616
|
+
const band1 = "_legendGradientBandStop";
|
|
617
|
+
const tick0 = "_legendGradientTickStart";
|
|
618
|
+
const tick1 = "_legendGradientTickStop";
|
|
619
|
+
const label = "_legendGradientLabelPosition";
|
|
620
|
+
/**
|
|
621
|
+
* @param {string} field
|
|
622
|
+
* @param {import("../spec/scale.js").Scale} scale
|
|
623
|
+
* @param {boolean} indexed
|
|
624
|
+
* @returns {import("../spec/channel.js").PositionFieldDef}
|
|
625
|
+
*/
|
|
626
|
+
const enc = (field, scale, indexed) =>
|
|
627
|
+
/** @type {import("../spec/channel.js").PositionFieldDef} */ ({
|
|
628
|
+
field,
|
|
629
|
+
type: "quantitative",
|
|
630
|
+
scale,
|
|
631
|
+
axis: null,
|
|
632
|
+
...(indexed ? { buildIndex: false } : {}),
|
|
633
|
+
});
|
|
634
|
+
/** @type {import("../spec/data.js").Data} */
|
|
635
|
+
const tickData = {
|
|
636
|
+
lazy: {
|
|
637
|
+
type: "legendGradientTicks",
|
|
638
|
+
channel,
|
|
639
|
+
count: DEFAULT_GRADIENT_TICK_COUNT,
|
|
640
|
+
format,
|
|
641
|
+
values: legend.values,
|
|
642
|
+
},
|
|
643
|
+
};
|
|
644
|
+
/** @type {import("../spec/transform.js").FormulaParams[]} */
|
|
645
|
+
const tickTransform = [
|
|
646
|
+
{
|
|
647
|
+
type: "formula",
|
|
648
|
+
expr: "" + (h ? tickY : tickX),
|
|
649
|
+
as: tick0,
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
type: "formula",
|
|
653
|
+
expr: "" + (h ? tickY2 : tickX2),
|
|
654
|
+
as: tick1,
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
type: "formula",
|
|
658
|
+
expr: "" + (h ? labelY : labelX),
|
|
659
|
+
as: label,
|
|
660
|
+
},
|
|
661
|
+
];
|
|
662
|
+
/** @type {import("../spec/transform.js").FormulaParams[]} */
|
|
663
|
+
const bandTransform = [
|
|
664
|
+
{
|
|
665
|
+
type: "formula",
|
|
666
|
+
expr: "" + (h ? gradientY : 0),
|
|
667
|
+
as: band0,
|
|
668
|
+
},
|
|
669
|
+
{
|
|
670
|
+
type: "formula",
|
|
671
|
+
expr: "" + (h ? gradientY2 : DEFAULT_GRADIENT_THICKNESS),
|
|
672
|
+
as: band1,
|
|
673
|
+
},
|
|
674
|
+
];
|
|
675
|
+
|
|
676
|
+
/** @type {(import("../spec/view.js").UnitSpec | import("../spec/view.js").LayerSpec)[]} */
|
|
677
|
+
const bodyLayer = [
|
|
678
|
+
{
|
|
679
|
+
name: "gradientRamp",
|
|
680
|
+
transform: bandTransform,
|
|
681
|
+
mark: {
|
|
682
|
+
type: "rect",
|
|
683
|
+
clip: false,
|
|
684
|
+
},
|
|
685
|
+
encoding: {
|
|
686
|
+
[p]: enc(h ? "position0" : "position1", ps, p == "x"),
|
|
687
|
+
[p2]: {
|
|
688
|
+
field: h ? "position1" : "position0",
|
|
689
|
+
type: "quantitative",
|
|
690
|
+
scale: ps,
|
|
691
|
+
},
|
|
692
|
+
[q]: enc(band0, qs, q == "x"),
|
|
693
|
+
[q2]: {
|
|
694
|
+
field: band1,
|
|
695
|
+
type: "quantitative",
|
|
696
|
+
scale: qs,
|
|
697
|
+
},
|
|
698
|
+
[channel]: {
|
|
699
|
+
field: "value",
|
|
700
|
+
type: "quantitative",
|
|
701
|
+
domainInert: true,
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
name: "gradientGuide",
|
|
707
|
+
data: tickData,
|
|
708
|
+
transform: tickTransform,
|
|
709
|
+
layer: [
|
|
710
|
+
{
|
|
711
|
+
name: "gradientTicks",
|
|
712
|
+
mark: {
|
|
713
|
+
type: "rule",
|
|
714
|
+
clip: false,
|
|
715
|
+
},
|
|
716
|
+
encoding: {
|
|
717
|
+
[p]: enc("position", ps, p == "x"),
|
|
718
|
+
[p2]: {
|
|
719
|
+
field: "position",
|
|
720
|
+
type: "quantitative",
|
|
721
|
+
scale: ps,
|
|
722
|
+
},
|
|
723
|
+
[q]: enc(tick0, qs, q == "x"),
|
|
724
|
+
[q2]: {
|
|
725
|
+
field: tick1,
|
|
726
|
+
type: "quantitative",
|
|
727
|
+
scale: qs,
|
|
728
|
+
},
|
|
729
|
+
},
|
|
730
|
+
},
|
|
731
|
+
{
|
|
732
|
+
name: "gradientLabels",
|
|
733
|
+
transform: [
|
|
734
|
+
{
|
|
735
|
+
type: "truncateText",
|
|
736
|
+
field: "label",
|
|
737
|
+
limit: legend.labelLimit,
|
|
738
|
+
fontSize: labelFontSize,
|
|
739
|
+
font: legend.labelFont,
|
|
740
|
+
fontStyle: legend.labelFontStyle,
|
|
741
|
+
fontWeight: legend.labelFontWeight,
|
|
742
|
+
},
|
|
743
|
+
{
|
|
744
|
+
type: "measureText",
|
|
745
|
+
field: "label",
|
|
746
|
+
as: LABEL_WIDTH_FIELD,
|
|
747
|
+
fontSize: labelFontSize,
|
|
748
|
+
font: legend.labelFont,
|
|
749
|
+
fontStyle: legend.labelFontStyle,
|
|
750
|
+
fontWeight: legend.labelFontWeight,
|
|
751
|
+
},
|
|
752
|
+
],
|
|
753
|
+
mark: {
|
|
754
|
+
type: "text",
|
|
755
|
+
clip: false,
|
|
756
|
+
align: labelAlign,
|
|
757
|
+
baseline: labelBaseline,
|
|
758
|
+
color: legend.labelColor,
|
|
759
|
+
font: legend.labelFont,
|
|
760
|
+
fontStyle: legend.labelFontStyle,
|
|
761
|
+
fontWeight: legend.labelFontWeight,
|
|
762
|
+
size: labelFontSize,
|
|
763
|
+
},
|
|
764
|
+
encoding: {
|
|
765
|
+
[p]: enc("position", ps, p == "x"),
|
|
766
|
+
[q]: enc(label, qs, q == "x"),
|
|
767
|
+
text: { field: "label" },
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
],
|
|
771
|
+
},
|
|
772
|
+
];
|
|
773
|
+
|
|
774
|
+
return createLegendRootSpec(
|
|
775
|
+
legend,
|
|
776
|
+
{
|
|
777
|
+
name: "gradientBody",
|
|
778
|
+
width: h
|
|
779
|
+
? { grow: 1, minPx: MIN_GRADIENT_LEGEND_LENGTH }
|
|
780
|
+
: undefined,
|
|
781
|
+
height: h
|
|
782
|
+
? { grow: 1 }
|
|
783
|
+
: { grow: 1, minPx: MIN_GRADIENT_LEGEND_LENGTH },
|
|
784
|
+
view: LEGEND_VIEW_BACKGROUND,
|
|
785
|
+
resolve: {
|
|
786
|
+
scale: { x: "excluded", y: "excluded" },
|
|
787
|
+
axis: { x: "excluded", y: "excluded" },
|
|
788
|
+
},
|
|
789
|
+
data: {
|
|
790
|
+
lazy: {
|
|
791
|
+
type: "legendGradient",
|
|
792
|
+
channel,
|
|
793
|
+
count: DEFAULT_GRADIENT_SAMPLE_COUNT,
|
|
794
|
+
},
|
|
795
|
+
},
|
|
796
|
+
layer: bodyLayer,
|
|
797
|
+
},
|
|
798
|
+
context,
|
|
799
|
+
[channel]
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* @param {{
|
|
805
|
+
* getPerpendicularSize: () => number,
|
|
806
|
+
* getOffset: () => number
|
|
807
|
+
* } | undefined} legendView
|
|
808
|
+
* @returns {number}
|
|
809
|
+
*/
|
|
810
|
+
export function getExternalLegendOverhang(legendView) {
|
|
811
|
+
return legendView
|
|
812
|
+
? legendView.getPerpendicularSize() + legendView.getOffset()
|
|
813
|
+
: 0;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export class LegendRegionView extends ContainerView {
|
|
817
|
+
/** @type {import("./view.js").default | undefined} */
|
|
818
|
+
#child;
|
|
819
|
+
|
|
820
|
+
/** @type {LegendView[]} */
|
|
821
|
+
#legendViews = [];
|
|
822
|
+
|
|
823
|
+
#stackSpacing;
|
|
824
|
+
|
|
825
|
+
/**
|
|
826
|
+
* @param {import("../spec/legend.js").LegendOrient} orient
|
|
827
|
+
* @param {number} stackSpacing
|
|
828
|
+
* @param {import("../types/viewContext.js").default} context
|
|
829
|
+
* @param {import("./containerView.js").default} layoutParent
|
|
830
|
+
* @param {import("./view.js").default} dataParent
|
|
831
|
+
*/
|
|
832
|
+
constructor(orient, stackSpacing, context, layoutParent, dataParent) {
|
|
833
|
+
super(
|
|
834
|
+
{
|
|
835
|
+
name: "legend_region_" + orient,
|
|
836
|
+
spacing: stackSpacing,
|
|
837
|
+
vconcat: [],
|
|
838
|
+
},
|
|
839
|
+
context,
|
|
840
|
+
layoutParent,
|
|
841
|
+
dataParent,
|
|
842
|
+
"legend_region_" + orient
|
|
843
|
+
);
|
|
844
|
+
|
|
845
|
+
this.needsAxes = { x: false, y: false };
|
|
846
|
+
this.orient = orient;
|
|
847
|
+
this.#stackSpacing = stackSpacing;
|
|
848
|
+
|
|
849
|
+
markViewAsNonAddressable(this, { skipSubtree: true });
|
|
850
|
+
markViewAsChrome(this, { skipSubtree: true });
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async initializeChildren() {
|
|
854
|
+
this.#child = await this.context.createOrImportView(
|
|
855
|
+
{
|
|
856
|
+
name: "legendStack",
|
|
857
|
+
spacing: this.#stackSpacing,
|
|
858
|
+
vconcat: [],
|
|
859
|
+
},
|
|
860
|
+
this,
|
|
861
|
+
this,
|
|
862
|
+
this.getNextAutoName("legendStack")
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
markViewAsNonAddressable(this.#child, { skipSubtree: true });
|
|
866
|
+
markViewAsChrome(this.#child, { skipSubtree: true });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* @param {LegendView} legendView
|
|
871
|
+
*/
|
|
872
|
+
addLegendView(legendView) {
|
|
873
|
+
if (!this.#child) {
|
|
874
|
+
throw new Error("Legend region has not been initialized!");
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
legendView.layoutParent =
|
|
878
|
+
/** @type {import("./containerView.js").default} */ (this.#child);
|
|
879
|
+
this.#legendViews.push(legendView);
|
|
880
|
+
/** @type {any} */ (this.#child).appendChildView(legendView);
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* @returns {LegendView[]}
|
|
885
|
+
*/
|
|
886
|
+
#getVisibleLegendViews() {
|
|
887
|
+
return this.#legendViews.filter((legendView) => legendView.isActive());
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
/**
|
|
891
|
+
* @returns {IterableIterator<import("./view.js").default>}
|
|
892
|
+
*/
|
|
893
|
+
*[Symbol.iterator]() {
|
|
894
|
+
if (this.#child) {
|
|
895
|
+
yield this.#child;
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
getSize() {
|
|
900
|
+
const mainSize = this.#getParallelSizeDef();
|
|
901
|
+
const perpendicularSize = { px: this.getPerpendicularSize() };
|
|
902
|
+
|
|
903
|
+
if (this.orient == "top" || this.orient == "bottom") {
|
|
904
|
+
return new FlexDimensions(mainSize, perpendicularSize);
|
|
905
|
+
} else {
|
|
906
|
+
return new FlexDimensions(perpendicularSize, mainSize);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/**
|
|
911
|
+
* @returns {import("./layout/flexLayout.js").SizeDef}
|
|
912
|
+
*/
|
|
913
|
+
#getParallelSizeDef() {
|
|
914
|
+
// Legend disable is reactive and separate from configured view
|
|
915
|
+
// visibility, so the internal legendStack concat cannot be used
|
|
916
|
+
// directly for region sizing.
|
|
917
|
+
const legendViews = this.#getVisibleLegendViews();
|
|
918
|
+
|
|
919
|
+
if (!legendViews.length) {
|
|
920
|
+
return { grow: 1 };
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
if (this.orient == "top" || this.orient == "bottom") {
|
|
924
|
+
return getLargestSize(
|
|
925
|
+
legendViews.map((legendView) => legendView.getSize().width)
|
|
926
|
+
);
|
|
927
|
+
} else {
|
|
928
|
+
/** @type {import("./layout/flexLayout.js").SizeDef[]} */
|
|
929
|
+
const sizeDefs = [];
|
|
930
|
+
for (const [index, legendView] of legendViews.entries()) {
|
|
931
|
+
if (index > 0) {
|
|
932
|
+
sizeDefs.push({ px: this.#stackSpacing, grow: 0 });
|
|
933
|
+
}
|
|
934
|
+
sizeDefs.push(legendView.getSize().height);
|
|
935
|
+
}
|
|
936
|
+
return sumSizeDefs(sizeDefs);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
getPerpendicularSize() {
|
|
941
|
+
if (!this.#child) {
|
|
942
|
+
return 0;
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
const legendViews = this.#getVisibleLegendViews();
|
|
946
|
+
|
|
947
|
+
if (this.orient == "top" || this.orient == "bottom") {
|
|
948
|
+
return legendViews.reduce(
|
|
949
|
+
(sum, legendView, index) =>
|
|
950
|
+
sum +
|
|
951
|
+
legendView.getPerpendicularSize() +
|
|
952
|
+
(index > 0 ? this.#stackSpacing : 0),
|
|
953
|
+
0
|
|
954
|
+
);
|
|
955
|
+
} else {
|
|
956
|
+
return Math.max(
|
|
957
|
+
0,
|
|
958
|
+
...legendViews.map((legendView) =>
|
|
959
|
+
legendView.getPerpendicularSize()
|
|
960
|
+
)
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
getWidth() {
|
|
966
|
+
return Math.max(
|
|
967
|
+
0,
|
|
968
|
+
...this.#getVisibleLegendViews().map((legendView) =>
|
|
969
|
+
getSizeDefMinPx(legendView.getSize().width)
|
|
970
|
+
)
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
getHeight() {
|
|
975
|
+
return this.#getVisibleLegendViews().reduce(
|
|
976
|
+
(sum, legendView, index) =>
|
|
977
|
+
sum +
|
|
978
|
+
getSizeDefMinPx(legendView.getSize().height) +
|
|
979
|
+
(index > 0 ? this.#stackSpacing : 0),
|
|
980
|
+
0
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
getOffset() {
|
|
985
|
+
return Math.max(
|
|
986
|
+
0,
|
|
987
|
+
...this.#getVisibleLegendViews().map((legendView) =>
|
|
988
|
+
legendView.getOffset()
|
|
989
|
+
)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
getParallelSize() {
|
|
994
|
+
const legendViews = this.#getVisibleLegendViews();
|
|
995
|
+
if (!legendViews.length) {
|
|
996
|
+
return 0;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
const parallelSize = this.#getParallelSizeDef();
|
|
1000
|
+
// Undefined asks legendLayout to stretch the region to the available
|
|
1001
|
+
// viewport size. Fixed stacks return a numeric natural extent.
|
|
1002
|
+
if (getSizeDefMaxPx(parallelSize) === undefined) {
|
|
1003
|
+
return undefined;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
return getSizeDefMinPx(parallelSize);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
isPickingSupported() {
|
|
1010
|
+
return false;
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* @param {import("./renderingContext/viewRenderingContext.js").default} context
|
|
1015
|
+
* @param {import("./layout/rectangle.js").default} coords
|
|
1016
|
+
* @param {import("../types/rendering.js").RenderingOptions} [options]
|
|
1017
|
+
*/
|
|
1018
|
+
render(context, coords, options = {}) {
|
|
1019
|
+
super.render(context, coords, options);
|
|
1020
|
+
|
|
1021
|
+
if (!this.isConfiguredVisible()) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
context.pushView(this, coords);
|
|
1026
|
+
const legendViews = this.#getVisibleLegendViews();
|
|
1027
|
+
const legendSizes = legendViews.map(
|
|
1028
|
+
(legendView) => legendView.getSize().height
|
|
1029
|
+
);
|
|
1030
|
+
const legendLocSizes = mapToPixelCoords(legendSizes, coords.height, {
|
|
1031
|
+
spacing: this.#stackSpacing,
|
|
1032
|
+
devicePixelRatio: context.getDevicePixelRatio(),
|
|
1033
|
+
});
|
|
1034
|
+
|
|
1035
|
+
for (const [index, legendView] of legendViews.entries()) {
|
|
1036
|
+
const locSize = legendLocSizes[index];
|
|
1037
|
+
const legendCoords = new Rectangle(
|
|
1038
|
+
() => coords.x,
|
|
1039
|
+
() => coords.y + locSize.location,
|
|
1040
|
+
() => coords.width,
|
|
1041
|
+
() => locSize.size
|
|
1042
|
+
);
|
|
1043
|
+
legendView.render(context, legendCoords, options);
|
|
1044
|
+
}
|
|
1045
|
+
context.popView(this);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
/**
|
|
1049
|
+
* @param {import("../utils/interaction.js").default} event
|
|
1050
|
+
*/
|
|
1051
|
+
propagateInteraction(event) {
|
|
1052
|
+
this.handleInteraction(event, true);
|
|
1053
|
+
this.#child?.propagateInteraction(event);
|
|
1054
|
+
this.handleInteraction(event, false);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
export default class LegendView extends ContainerView {
|
|
1059
|
+
#effectiveExtent;
|
|
1060
|
+
|
|
1061
|
+
/** @type {import("./view.js").default | undefined} */
|
|
1062
|
+
#child;
|
|
1063
|
+
|
|
1064
|
+
/** @type {"symbol" | "gradient"} */
|
|
1065
|
+
#type;
|
|
1066
|
+
|
|
1067
|
+
/** @type {() => boolean} */
|
|
1068
|
+
#activePredicate = () => true;
|
|
1069
|
+
|
|
1070
|
+
/** @type {MeasuredLabels | undefined} */
|
|
1071
|
+
#measuredLabels;
|
|
1072
|
+
|
|
1073
|
+
#stackedParallelSize = 0;
|
|
1074
|
+
|
|
1075
|
+
/** @type {UnitView[]} */
|
|
1076
|
+
#labelViews = [];
|
|
1077
|
+
|
|
1078
|
+
/** @type {Set<import("../data/collector.js").default>} */
|
|
1079
|
+
#observedCollectors = new Set();
|
|
1080
|
+
|
|
1081
|
+
#measurementScheduled = false;
|
|
1082
|
+
|
|
1083
|
+
/**
|
|
1084
|
+
* @param {object} props
|
|
1085
|
+
* @param {LegendEntry[]} [props.entries]
|
|
1086
|
+
* @param {import("../spec/channel.js").ChannelWithScale} props.channel
|
|
1087
|
+
* @param {Partial<Record<import("../spec/channel.js").ChannelWithScale, string>>} [props.symbolChannels]
|
|
1088
|
+
* @param {SymbolLegendStyle} [props.symbolStyle]
|
|
1089
|
+
* @param {"point" | "stroke"} [props.symbolGeometry]
|
|
1090
|
+
* @param {"symbol" | "gradient"} [props.type]
|
|
1091
|
+
* @param {LegendConfig} props.legend
|
|
1092
|
+
* @param {string} [props.format]
|
|
1093
|
+
* @param {import("../spec/channel.js").Type} props.dataType
|
|
1094
|
+
* @param {import("../types/viewContext.js").default} context
|
|
1095
|
+
* @param {import("./containerView.js").default} layoutParent
|
|
1096
|
+
* @param {import("./view.js").default} dataParent
|
|
1097
|
+
* @param {import("./view.js").ViewOptions} [options]
|
|
1098
|
+
*/
|
|
1099
|
+
constructor(
|
|
1100
|
+
{
|
|
1101
|
+
entries,
|
|
1102
|
+
channel,
|
|
1103
|
+
symbolChannels,
|
|
1104
|
+
symbolStyle,
|
|
1105
|
+
symbolGeometry,
|
|
1106
|
+
type,
|
|
1107
|
+
legend,
|
|
1108
|
+
format,
|
|
1109
|
+
dataType,
|
|
1110
|
+
},
|
|
1111
|
+
context,
|
|
1112
|
+
layoutParent,
|
|
1113
|
+
dataParent,
|
|
1114
|
+
options
|
|
1115
|
+
) {
|
|
1116
|
+
const spec =
|
|
1117
|
+
type == "gradient"
|
|
1118
|
+
? createGradientLegendSpec({
|
|
1119
|
+
channel,
|
|
1120
|
+
legend,
|
|
1121
|
+
format,
|
|
1122
|
+
context,
|
|
1123
|
+
})
|
|
1124
|
+
: createSymbolLegendSpec({
|
|
1125
|
+
entries,
|
|
1126
|
+
channel,
|
|
1127
|
+
symbolChannels,
|
|
1128
|
+
symbolStyle,
|
|
1129
|
+
symbolGeometry,
|
|
1130
|
+
legend,
|
|
1131
|
+
format,
|
|
1132
|
+
dataType,
|
|
1133
|
+
context,
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
super(
|
|
1137
|
+
spec,
|
|
1138
|
+
context,
|
|
1139
|
+
layoutParent,
|
|
1140
|
+
dataParent,
|
|
1141
|
+
"legend_" + (legend.orient ?? "right"),
|
|
1142
|
+
options
|
|
1143
|
+
);
|
|
1144
|
+
|
|
1145
|
+
this.needsAxes = { x: false, y: false };
|
|
1146
|
+
this.legendProps = legend;
|
|
1147
|
+
this.#type = type ?? "symbol";
|
|
1148
|
+
this.#effectiveExtent = getMinimumLegendExtent(
|
|
1149
|
+
this.#type,
|
|
1150
|
+
this.legendProps
|
|
1151
|
+
);
|
|
1152
|
+
|
|
1153
|
+
markViewAsNonAddressable(this, { skipSubtree: true });
|
|
1154
|
+
markViewAsChrome(this, { skipSubtree: true });
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
async initializeChildren() {
|
|
1158
|
+
const childSpec = { ...this.spec };
|
|
1159
|
+
delete childSpec.name;
|
|
1160
|
+
|
|
1161
|
+
this.#child = await this.context.createOrImportView(
|
|
1162
|
+
childSpec,
|
|
1163
|
+
this,
|
|
1164
|
+
this,
|
|
1165
|
+
this.getNextAutoName("legend"),
|
|
1166
|
+
undefined,
|
|
1167
|
+
{
|
|
1168
|
+
// Generated legend internals use `width`/`height` expressions
|
|
1169
|
+
// for pixel-aware helper transforms and scales. Force local
|
|
1170
|
+
// layout params so authored specs that intentionally overload
|
|
1171
|
+
// these names, such as SampleView's sample-facet `height`, do
|
|
1172
|
+
// not leak into packLegendLabels or gradient scales. This should
|
|
1173
|
+
// become unnecessary once unit coordinate space has been
|
|
1174
|
+
// migrated to pixel space.
|
|
1175
|
+
layoutSizeParams: "force",
|
|
1176
|
+
}
|
|
1177
|
+
);
|
|
1178
|
+
|
|
1179
|
+
markViewAsNonAddressable(this.#child, { skipSubtree: true });
|
|
1180
|
+
markViewAsChrome(this.#child, { skipSubtree: true });
|
|
1181
|
+
|
|
1182
|
+
this.#labelViews = [];
|
|
1183
|
+
for (const view of this.getDescendants()) {
|
|
1184
|
+
if (
|
|
1185
|
+
view instanceof UnitView &&
|
|
1186
|
+
(view.name === "labels" || view.name === "gradientLabels")
|
|
1187
|
+
) {
|
|
1188
|
+
this.#labelViews.push(view);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
if (this.#labelViews.length > 0) {
|
|
1193
|
+
this.registerDisposer(
|
|
1194
|
+
this._addBroadcastHandler("subtreeDataReady", () =>
|
|
1195
|
+
this.#ensureLabelObservers()
|
|
1196
|
+
)
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
this.#stackedParallelSize = this.getStackedParallelSize();
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* @returns {IterableIterator<import("./view.js").default>}
|
|
1205
|
+
*/
|
|
1206
|
+
*[Symbol.iterator]() {
|
|
1207
|
+
if (this.#child) {
|
|
1208
|
+
yield this.#child;
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
getSize() {
|
|
1213
|
+
if (!this.isActive()) {
|
|
1214
|
+
return new FlexDimensions({ px: 0, grow: 0 }, { px: 0, grow: 0 });
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const contentHorizontal = isHorizontalLegend(this.legendProps);
|
|
1218
|
+
const mainSize = { grow: 1 };
|
|
1219
|
+
const perpendicularSize = { px: this.getPerpendicularSize() };
|
|
1220
|
+
const parallelSize = this.#hasFlexibleParallelSize()
|
|
1221
|
+
? this.#getFlexibleStackedParallelSize()
|
|
1222
|
+
: { px: this.getStackedParallelSize() };
|
|
1223
|
+
|
|
1224
|
+
if (isTopBottomLegend(this.legendProps)) {
|
|
1225
|
+
return new FlexDimensions(mainSize, perpendicularSize);
|
|
1226
|
+
} else if (contentHorizontal) {
|
|
1227
|
+
return new FlexDimensions(parallelSize, perpendicularSize);
|
|
1228
|
+
} else {
|
|
1229
|
+
return new FlexDimensions(perpendicularSize, parallelSize);
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* @returns {import("./layout/flexLayout.js").SizeDef}
|
|
1235
|
+
*/
|
|
1236
|
+
#getFlexibleStackedParallelSize() {
|
|
1237
|
+
if (!this.#child) {
|
|
1238
|
+
return { grow: 1, minPx: MIN_GRADIENT_LEGEND_LENGTH };
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// The generated child concat includes title, padding, and the gradient
|
|
1242
|
+
// body minimum, so use it as the public stacked parallel contract.
|
|
1243
|
+
const childSize = this.#child.getSize();
|
|
1244
|
+
return isHorizontalLegend(this.legendProps)
|
|
1245
|
+
? childSize.width
|
|
1246
|
+
: childSize.height;
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
#hasFlexibleParallelSize() {
|
|
1250
|
+
return (
|
|
1251
|
+
this.#type == "gradient" && !isHorizontalLegend(this.legendProps)
|
|
1252
|
+
);
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
getPerpendicularSize() {
|
|
1256
|
+
return this.#effectiveExtent;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
getOffset() {
|
|
1260
|
+
return this.legendProps.offset ?? 0;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
/**
|
|
1264
|
+
* @param {() => boolean} predicate
|
|
1265
|
+
*/
|
|
1266
|
+
setActivePredicate(predicate) {
|
|
1267
|
+
this.#activePredicate = predicate;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/*
|
|
1271
|
+
* Keep active visibility separate from configured visibility. View data
|
|
1272
|
+
* initialization uses isConfiguredVisible(), so reactive legend.disable
|
|
1273
|
+
* must not make the legend subtree look unconfigured. Otherwise a legend
|
|
1274
|
+
* that starts hidden would not initialize its marks and could not reappear.
|
|
1275
|
+
*/
|
|
1276
|
+
isActive() {
|
|
1277
|
+
return super.isConfiguredVisible() && this.#activePredicate();
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
getStackedParallelSize() {
|
|
1281
|
+
const measuredLabels =
|
|
1282
|
+
this.#measuredLabels ?? getMeasuredLabels(this.#labelViews);
|
|
1283
|
+
|
|
1284
|
+
return getStackedLegendParallelSize(
|
|
1285
|
+
this.legendProps,
|
|
1286
|
+
this.#type,
|
|
1287
|
+
measuredLabels,
|
|
1288
|
+
this.context
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
#scheduleAutoExtentMeasurement() {
|
|
1293
|
+
if (this.#measurementScheduled) {
|
|
1294
|
+
return;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
this.#measurementScheduled = true;
|
|
1298
|
+
queueMicrotask(() => {
|
|
1299
|
+
this.#measurementScheduled = false;
|
|
1300
|
+
this.#updateAutoExtent();
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
#ensureLabelObservers() {
|
|
1305
|
+
for (const labelsView of this.#labelViews) {
|
|
1306
|
+
const collector = labelsView.getCollector();
|
|
1307
|
+
if (!collector || this.#observedCollectors.has(collector)) {
|
|
1308
|
+
continue;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
this.#observedCollectors.add(collector);
|
|
1312
|
+
this.registerDisposer(
|
|
1313
|
+
collector.observe(() => this.#scheduleAutoExtentMeasurement())
|
|
1314
|
+
);
|
|
1315
|
+
|
|
1316
|
+
if (collector.completed) {
|
|
1317
|
+
this.#scheduleAutoExtentMeasurement();
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
#updateAutoExtent() {
|
|
1323
|
+
const measuredLabels = getMeasuredLabels(this.#labelViews);
|
|
1324
|
+
if (measuredLabels === undefined) {
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
const previousStackedParallelSize = this.#stackedParallelSize;
|
|
1329
|
+
this.#measuredLabels = measuredLabels;
|
|
1330
|
+
|
|
1331
|
+
const nextExtent = getLegendExtent(
|
|
1332
|
+
this.legendProps,
|
|
1333
|
+
this.#type,
|
|
1334
|
+
measuredLabels,
|
|
1335
|
+
this.context
|
|
1336
|
+
);
|
|
1337
|
+
const willGrow =
|
|
1338
|
+
nextExtent >= this.#effectiveExtent + AUTO_EXTENT_GROW_THRESHOLD_PX;
|
|
1339
|
+
const nextStackedParallelSize = this.getStackedParallelSize();
|
|
1340
|
+
const willResizeStack =
|
|
1341
|
+
Math.abs(nextStackedParallelSize - previousStackedParallelSize) >=
|
|
1342
|
+
AUTO_EXTENT_GROW_THRESHOLD_PX;
|
|
1343
|
+
|
|
1344
|
+
if (!willGrow && !willResizeStack) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
if (willGrow) {
|
|
1349
|
+
this.#effectiveExtent = nextExtent;
|
|
1350
|
+
}
|
|
1351
|
+
this.#stackedParallelSize = nextStackedParallelSize;
|
|
1352
|
+
this.invalidateSizeCache();
|
|
1353
|
+
this.context.requestLayoutReflow();
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Defers legend data updates that only serve layout packing. Scale-backed
|
|
1358
|
+
* legend marks keep using the live scale while callers run smooth layout
|
|
1359
|
+
* transitions.
|
|
1360
|
+
*
|
|
1361
|
+
* TODO: Prefer optimizing layout and render batching to support partial
|
|
1362
|
+
* updates, so transition callers do not need to suppress legend dataflow.
|
|
1363
|
+
*
|
|
1364
|
+
* @returns {() => void}
|
|
1365
|
+
*/
|
|
1366
|
+
suspendLayoutDataUpdates() {
|
|
1367
|
+
/** @type {Set<{suspendRangeUpdates: () => () => void}>} */
|
|
1368
|
+
const dataSources = new Set();
|
|
1369
|
+
for (const descendant of this.getDescendants()) {
|
|
1370
|
+
const dataSource = descendant.flowHandle?.dataSource;
|
|
1371
|
+
if (dataSource && "suspendRangeUpdates" in dataSource) {
|
|
1372
|
+
dataSources.add(
|
|
1373
|
+
/** @type {{suspendRangeUpdates: () => () => void}} */ (
|
|
1374
|
+
dataSource
|
|
1375
|
+
)
|
|
1376
|
+
);
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const releases = Array.from(dataSources).map((dataSource) =>
|
|
1381
|
+
dataSource.suspendRangeUpdates()
|
|
1382
|
+
);
|
|
1383
|
+
|
|
1384
|
+
return () => {
|
|
1385
|
+
for (const release of releases) {
|
|
1386
|
+
release();
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
isPickingSupported() {
|
|
1392
|
+
return false;
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
/**
|
|
1396
|
+
* @param {import("./renderingContext/viewRenderingContext.js").default} context
|
|
1397
|
+
* @param {import("./layout/rectangle.js").default} coords
|
|
1398
|
+
* @param {import("../types/rendering.js").RenderingOptions} [options]
|
|
1399
|
+
*/
|
|
1400
|
+
render(context, coords, options = {}) {
|
|
1401
|
+
super.render(context, coords, options);
|
|
1402
|
+
|
|
1403
|
+
if (!this.isActive()) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
context.pushView(this, coords);
|
|
1408
|
+
this.#child?.render(context, coords, options);
|
|
1409
|
+
context.popView(this);
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
/**
|
|
1413
|
+
* @param {import("../utils/interaction.js").default} event
|
|
1414
|
+
*/
|
|
1415
|
+
propagateInteraction(event) {
|
|
1416
|
+
this.handleInteraction(event, true);
|
|
1417
|
+
this.#child?.propagateInteraction(event);
|
|
1418
|
+
this.handleInteraction(event, false);
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
/**
|
|
1423
|
+
* @typedef {object} MeasuredLabels
|
|
1424
|
+
* @prop {number} maxWidth
|
|
1425
|
+
* @prop {number} maxEntryWidth
|
|
1426
|
+
* @prop {number} maxX
|
|
1427
|
+
* @prop {number} maxY
|
|
1428
|
+
*/
|
|
1429
|
+
|
|
1430
|
+
/**
|
|
1431
|
+
* @param {UnitView[]} labelViews
|
|
1432
|
+
* @returns {MeasuredLabels | undefined}
|
|
1433
|
+
*/
|
|
1434
|
+
function getMeasuredLabels(labelViews) {
|
|
1435
|
+
let maxWidth = 0;
|
|
1436
|
+
let maxEntryWidth = 0;
|
|
1437
|
+
let maxX = 0;
|
|
1438
|
+
let maxY = 0;
|
|
1439
|
+
let completed = false;
|
|
1440
|
+
|
|
1441
|
+
for (const labelsView of labelViews) {
|
|
1442
|
+
const collector = labelsView.getCollector();
|
|
1443
|
+
if (!collector?.completed) {
|
|
1444
|
+
return undefined;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
completed = true;
|
|
1448
|
+
collector.visitData((datum) => {
|
|
1449
|
+
maxWidth = Math.max(
|
|
1450
|
+
maxWidth,
|
|
1451
|
+
Number(datum[LABEL_WIDTH_FIELD]) || 0
|
|
1452
|
+
);
|
|
1453
|
+
maxEntryWidth = Math.max(
|
|
1454
|
+
maxEntryWidth,
|
|
1455
|
+
Number(datum.entryWidth) || 0
|
|
1456
|
+
);
|
|
1457
|
+
maxX = Math.max(
|
|
1458
|
+
maxX,
|
|
1459
|
+
(Number(datum.labelX) || 0) +
|
|
1460
|
+
(Number(datum[LABEL_WIDTH_FIELD]) || 0)
|
|
1461
|
+
);
|
|
1462
|
+
maxY = Math.max(maxY, Number(datum.labelY) || 0);
|
|
1463
|
+
});
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
return completed
|
|
1467
|
+
? {
|
|
1468
|
+
maxWidth: Math.ceil(maxWidth),
|
|
1469
|
+
maxEntryWidth: Math.ceil(maxEntryWidth),
|
|
1470
|
+
maxX: Math.ceil(maxX),
|
|
1471
|
+
maxY: Math.ceil(maxY),
|
|
1472
|
+
}
|
|
1473
|
+
: undefined;
|
|
1474
|
+
}
|
|
1475
|
+
|
|
1476
|
+
/**
|
|
1477
|
+
* @param {LegendConfig} legend
|
|
1478
|
+
* @param {"symbol" | "gradient"} type
|
|
1479
|
+
* @param {MeasuredLabels} measuredLabels
|
|
1480
|
+
* @param {import("../types/viewContext.js").default} context
|
|
1481
|
+
*/
|
|
1482
|
+
function getLegendExtent(legend, type, measuredLabels, context) {
|
|
1483
|
+
if (isHorizontalLegend(legend)) {
|
|
1484
|
+
return getHorizontalLegendExtent(legend, type, measuredLabels);
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
const titleWidth = isSideTitle(legend)
|
|
1488
|
+
? getTitleWidthWithPadding(legend, context)
|
|
1489
|
+
: getTitleWidth(legend, context);
|
|
1490
|
+
const labelOffset = legend.labelOffset ?? 4;
|
|
1491
|
+
const labelExtent =
|
|
1492
|
+
type == "gradient"
|
|
1493
|
+
? DEFAULT_GRADIENT_THICKNESS +
|
|
1494
|
+
DEFAULT_GRADIENT_TICK_SIZE +
|
|
1495
|
+
labelOffset +
|
|
1496
|
+
measuredLabels.maxWidth
|
|
1497
|
+
: measuredLabels.maxEntryWidth ||
|
|
1498
|
+
Math.sqrt(legend.symbolSize ?? 100) +
|
|
1499
|
+
(legend.symbolStrokeWidth ?? 1.5) +
|
|
1500
|
+
labelOffset +
|
|
1501
|
+
measuredLabels.maxWidth;
|
|
1502
|
+
|
|
1503
|
+
return Math.ceil(
|
|
1504
|
+
Math.max(
|
|
1505
|
+
getMinimumLegendExtent(type, legend),
|
|
1506
|
+
isSideTitle(legend) ? labelExtent + titleWidth : labelExtent,
|
|
1507
|
+
titleWidth
|
|
1508
|
+
)
|
|
1509
|
+
);
|
|
1510
|
+
}
|
|
1511
|
+
|
|
1512
|
+
/**
|
|
1513
|
+
* @param {"symbol" | "gradient"} type
|
|
1514
|
+
* @param {LegendConfig} legend
|
|
1515
|
+
*/
|
|
1516
|
+
function getMinimumLegendExtent(type, legend) {
|
|
1517
|
+
if (isHorizontalLegend(legend)) {
|
|
1518
|
+
return type == "gradient" ? 32 : 0;
|
|
1519
|
+
} else {
|
|
1520
|
+
return 0;
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
/**
|
|
1525
|
+
* @param {LegendConfig} legend
|
|
1526
|
+
* @param {"symbol" | "gradient"} type
|
|
1527
|
+
* @param {MeasuredLabels | undefined} measuredLabels
|
|
1528
|
+
* @param {import("../types/viewContext.js").default} context
|
|
1529
|
+
*/
|
|
1530
|
+
function getStackedLegendParallelSize(legend, type, measuredLabels, context) {
|
|
1531
|
+
const titleExtent = getParallelTitleExtent(legend, context);
|
|
1532
|
+
const titleSideBySide = isSideTitle(legend);
|
|
1533
|
+
const combine = (/** @type {number} */ bodyExtent) =>
|
|
1534
|
+
isHorizontalLegend(legend) == titleSideBySide
|
|
1535
|
+
? Math.ceil(titleExtent + bodyExtent)
|
|
1536
|
+
: Math.ceil(Math.max(titleExtent, bodyExtent));
|
|
1537
|
+
|
|
1538
|
+
if (type == "gradient") {
|
|
1539
|
+
return combine(DEFAULT_GRADIENT_LEGEND_LENGTH);
|
|
1540
|
+
} else if (measuredLabels) {
|
|
1541
|
+
const labelFontSize = legend.labelFontSize ?? 10;
|
|
1542
|
+
const bodyExtent = isHorizontalLegend(legend)
|
|
1543
|
+
? measuredLabels.maxX
|
|
1544
|
+
: measuredLabels.maxY + labelFontSize / 2;
|
|
1545
|
+
|
|
1546
|
+
return combine(bodyExtent);
|
|
1547
|
+
} else {
|
|
1548
|
+
return combine(
|
|
1549
|
+
Math.sqrt(legend.symbolSize ?? 100) +
|
|
1550
|
+
(legend.symbolStrokeWidth ?? 1.5)
|
|
1551
|
+
);
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
/**
|
|
1556
|
+
* @param {LegendConfig} legend
|
|
1557
|
+
* @param {"symbol" | "gradient"} type
|
|
1558
|
+
* @param {MeasuredLabels} measuredLabels
|
|
1559
|
+
*/
|
|
1560
|
+
function getHorizontalLegendExtent(legend, type, measuredLabels) {
|
|
1561
|
+
const labelFontSize = legend.labelFontSize ?? 10;
|
|
1562
|
+
const labelOffset = legend.labelOffset ?? 4;
|
|
1563
|
+
const titleExtent = getPerpendicularTitleExtent(legend);
|
|
1564
|
+
const bodyExtent =
|
|
1565
|
+
type == "gradient"
|
|
1566
|
+
? labelFontSize +
|
|
1567
|
+
labelOffset +
|
|
1568
|
+
DEFAULT_GRADIENT_TICK_SIZE +
|
|
1569
|
+
DEFAULT_GRADIENT_THICKNESS +
|
|
1570
|
+
2
|
|
1571
|
+
: measuredLabels.maxY + labelFontSize / 2;
|
|
1572
|
+
|
|
1573
|
+
return Math.ceil(
|
|
1574
|
+
Math.max(
|
|
1575
|
+
getMinimumLegendExtent(type, legend),
|
|
1576
|
+
isSideTitle(legend)
|
|
1577
|
+
? Math.max(titleExtent, bodyExtent)
|
|
1578
|
+
: titleExtent + bodyExtent
|
|
1579
|
+
)
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
/**
|
|
1584
|
+
* @param {LegendConfig} legend
|
|
1585
|
+
*/
|
|
1586
|
+
function getTitleHeight(legend) {
|
|
1587
|
+
return legend.title ? (legend.titleFontSize ?? 11) : 0;
|
|
1588
|
+
}
|
|
1589
|
+
|
|
1590
|
+
/**
|
|
1591
|
+
* @param {LegendConfig} legend
|
|
1592
|
+
*/
|
|
1593
|
+
function getPerpendicularTitleExtent(legend) {
|
|
1594
|
+
return legend.title
|
|
1595
|
+
? getTitleHeight(legend) +
|
|
1596
|
+
(isSideTitle(legend) ? 0 : (legend.titlePadding ?? 5))
|
|
1597
|
+
: 0;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* @param {LegendConfig} legend
|
|
1602
|
+
* @param {import("../types/viewContext.js").default} context
|
|
1603
|
+
*/
|
|
1604
|
+
function getParallelTitleExtent(legend, context) {
|
|
1605
|
+
return isSideTitle(legend)
|
|
1606
|
+
? getTitleWidthWithPadding(legend, context)
|
|
1607
|
+
: getPerpendicularTitleExtent(legend);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* @param {LegendConfig} legend
|
|
1612
|
+
* @param {import("../types/viewContext.js").default} context
|
|
1613
|
+
*/
|
|
1614
|
+
function getTitleWidthWithPadding(legend, context) {
|
|
1615
|
+
return legend.title
|
|
1616
|
+
? getTitleWidth(legend, context) + (legend.titlePadding ?? 5)
|
|
1617
|
+
: 0;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
/**
|
|
1621
|
+
* @param {LegendConfig} legend
|
|
1622
|
+
* @param {import("../types/viewContext.js").default} context
|
|
1623
|
+
*/
|
|
1624
|
+
function getTitleWidth(legend, context) {
|
|
1625
|
+
if (!legend.title) {
|
|
1626
|
+
return 0;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
const font = requestFont(context.fontManager, {
|
|
1630
|
+
font: legend.titleFont,
|
|
1631
|
+
fontStyle: legend.titleFontStyle,
|
|
1632
|
+
fontWeight: legend.titleFontWeight,
|
|
1633
|
+
});
|
|
1634
|
+
// Generated legend title width is materialized into the child spec before
|
|
1635
|
+
// asynchronous font loading has necessarily completed. Use fallback metrics
|
|
1636
|
+
// to avoid a padding-only title extent. TODO: Recompute generated legend
|
|
1637
|
+
// specs or make title extent lazy when requested font metrics become ready.
|
|
1638
|
+
const metrics =
|
|
1639
|
+
font.metrics ?? context.fontManager.getDefaultFont().metrics;
|
|
1640
|
+
if (!metrics) {
|
|
1641
|
+
return 0;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const fontSize = legend.titleFontSize ?? 11;
|
|
1645
|
+
const text = truncateText(
|
|
1646
|
+
legend.title,
|
|
1647
|
+
legend.titleLimit,
|
|
1648
|
+
(text, fontSize) => measureText(metrics, text, fontSize).width,
|
|
1649
|
+
fontSize,
|
|
1650
|
+
"..."
|
|
1651
|
+
);
|
|
1652
|
+
|
|
1653
|
+
return measureText(metrics, text, fontSize).width;
|
|
1654
|
+
}
|