ggaction 0.0.13 → 0.0.16
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/CHANGELOG.md +146 -0
- package/README.md +41 -12
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +39 -7
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/basic.js +2 -2
- package/src/actions/boxPlots/components.js +20 -12
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +34 -9
- package/src/actions/boxPlots/materialize.js +12 -4
- package/src/actions/boxPlots/options.js +48 -16
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +21 -12
- package/src/actions/charts/area.js +6 -2
- package/src/actions/charts/bar.js +3 -2
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +7 -3
- package/src/actions/charts/ecdf.js +6 -3
- package/src/actions/charts/endpoints.js +29 -7
- package/src/actions/charts/heatmap.js +3 -2
- package/src/actions/charts/histogram.js +5 -4
- package/src/actions/charts/horizon.js +4 -2
- package/src/actions/charts/interval-regression.js +7 -4
- package/src/actions/charts/line.js +4 -2
- package/src/actions/charts/parallel.js +9 -3
- package/src/actions/charts/pie.js +11 -3
- package/src/actions/charts/polar.js +17 -6
- package/src/actions/charts/radar.js +9 -3
- package/src/actions/charts/radial.js +12 -4
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +8 -4
- package/src/actions/charts/scatter.js +4 -2
- package/src/actions/composition/actions.js +29 -24
- package/src/actions/coordinates/actions.js +2 -2
- package/src/actions/coordinates/edit.js +76 -0
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +3 -3
- package/src/actions/data/bin2d.js +63 -193
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +26 -0
- package/src/actions/data/computed.js +4 -4
- package/src/actions/data/create.js +7 -14
- package/src/actions/data/density.js +15 -23
- package/src/actions/data/derived.js +57 -31
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +551 -0
- package/src/actions/data/filter.js +28 -22
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +26 -0
- package/src/actions/data/index.js +62 -47
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +27 -0
- package/src/actions/data/regression.js +21 -27
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +125 -0
- package/src/actions/data/shared.js +20 -4
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +3 -3
- package/src/actions/data/timeUnit.js +6 -3
- package/src/actions/data/window.js +3 -3
- package/src/actions/encodings/angle.js +1 -1
- package/src/actions/encodings/appearance.js +4 -4
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +415 -0
- package/src/actions/encodings/color/index.js +3 -2
- package/src/actions/encodings/density.js +7 -4
- package/src/actions/encodings/histogram.js +4 -3
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/index.js +4 -0
- package/src/actions/encodings/offset.js +22 -15
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/apply.js +53 -7
- package/src/actions/encodings/position/index.js +14 -6
- package/src/actions/encodings/position/policies/bar.js +9 -0
- package/src/actions/encodings/position/resolve.js +24 -7
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +13 -9
- package/src/actions/encodings/ruleAppearance.js +2 -24
- package/src/actions/encodings/seriesLayout.js +2 -1
- package/src/actions/encodings/shared.js +82 -14
- package/src/actions/encodings/stroke.js +226 -0
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +34 -20
- package/src/actions/errorBands/edit.js +34 -137
- package/src/actions/errorBars/create.js +43 -20
- package/src/actions/errorBars/edit.js +29 -125
- package/src/actions/facets/actions.js +267 -73
- package/src/actions/facets/derive.js +99 -28
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +15 -8
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +18 -9
- package/src/actions/gradientPlots/materialize.js +5 -3
- package/src/actions/gradientPlots/options.js +10 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/applicability.js +8 -2
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +67 -21
- package/src/actions/guides/axes/lines.js +15 -9
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel/resolve.js +6 -2
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +6 -5
- package/src/actions/guides/axes/ticks.js +19 -6
- package/src/actions/guides/axes/titles.js +21 -13
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/grids/resolve.js +7 -2
- package/src/actions/guides/guides.js +20 -4
- package/src/actions/guides/legends/blocks.js +426 -0
- package/src/actions/guides/legends/categorical/actions.js +138 -52
- package/src/actions/guides/legends/categorical/components.js +55 -45
- package/src/actions/guides/legends/categorical/index.js +37 -33
- package/src/actions/guides/legends/categorical/layout.js +98 -16
- package/src/actions/guides/legends/categorical/options.js +2 -2
- package/src/actions/guides/legends/categorical/recipes.js +111 -20
- package/src/actions/guides/legends/categorical/resolve.js +12 -8
- package/src/actions/guides/legends/categorical/symbols.js +104 -52
- package/src/actions/guides/legends/continuous/common.js +76 -20
- package/src/actions/guides/legends/continuous/gradient.js +95 -158
- package/src/actions/guides/legends/continuous/index.js +2 -0
- package/src/actions/guides/legends/continuous/interval.js +21 -16
- package/src/actions/guides/legends/continuous/opacity.js +37 -83
- package/src/actions/guides/legends/continuous/stroke.js +225 -0
- package/src/actions/guides/legends/creation.js +14 -1
- package/src/actions/guides/legends/edit.js +159 -60
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/index.js +2 -0
- package/src/actions/guides/legends/lane.js +59 -24
- package/src/actions/guides/legends/lifecycle.js +19 -8
- package/src/actions/guides/legends/remove.js +17 -8
- package/src/actions/guides/legends/sampling.js +140 -0
- package/src/actions/guides/legends/size.js +100 -108
- package/src/actions/guides/legends/strokeWidth.js +38 -92
- package/src/actions/guides/legends/target.js +112 -1
- package/src/actions/guides/legends/transition.js +165 -16
- package/src/actions/guides/polar/axes/facade.js +8 -8
- package/src/actions/guides/polar/axes/labels.js +29 -10
- package/src/actions/guides/polar/axes/lines.js +19 -19
- package/src/actions/guides/polar/axes/shared.js +16 -0
- package/src/actions/guides/polar/axes/ticks.js +10 -16
- package/src/actions/guides/polar/axes/titles.js +6 -6
- package/src/actions/guides/polar/grids.js +12 -14
- package/src/actions/guides/polar/resolve.js +12 -8
- package/src/actions/index.js +16 -3
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +80 -22
- package/src/actions/marks/area/actions.js +38 -13
- package/src/actions/marks/area/materialize.js +17 -1
- package/src/actions/marks/bar/create.js +12 -3
- package/src/actions/marks/bar/edit.js +21 -4
- package/src/actions/marks/bar/materialize.js +34 -40
- package/src/actions/marks/index.js +10 -1
- package/src/actions/marks/line/actions.js +40 -17
- package/src/actions/marks/line/materialize.js +29 -10
- package/src/actions/marks/point/create.js +9 -3
- package/src/actions/marks/point/edit.js +19 -7
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +48 -22
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +36 -15
- package/src/actions/marks/references.js +602 -33
- package/src/actions/marks/remove.js +67 -41
- package/src/actions/marks/rule/actions.js +51 -14
- package/src/actions/marks/shared.js +1 -1
- package/src/actions/marks/text/actions.js +79 -14
- package/src/actions/marks/text/index.js +4 -0
- package/src/actions/marks/text/layout.js +157 -17
- package/src/actions/marks/text/removal.js +173 -0
- package/src/actions/marks/text/selection.js +72 -0
- package/src/actions/marks/tick/actions.js +25 -8
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +31 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +11 -3
- package/src/actions/primitives/semanticAction.js +90 -14
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/primitives/semanticValidation/guide.js +1 -1
- package/src/actions/primitives/semanticValidation/index.js +9 -1
- package/src/actions/primitives/semanticValidation/layer.js +5 -0
- package/src/actions/primitives/semanticValidation/scale.js +28 -7
- package/src/actions/regression/components.js +36 -25
- package/src/actions/regression/create.js +13 -6
- package/src/actions/regression/edit.js +24 -11
- package/src/actions/resources/index.js +7 -0
- package/src/actions/resources/remove.js +173 -0
- package/src/actions/scales/channels.js +212 -0
- package/src/actions/scales/consumers/common.js +1 -1
- package/src/actions/scales/consumers/families.js +3 -2
- package/src/actions/scales/consumers/index.js +19 -2
- package/src/actions/scales/consumers/seriesLayout.js +28 -14
- package/src/actions/scales/create.js +55 -23
- package/src/actions/scales/definitions.js +94 -40
- package/src/actions/scales/edit.js +67 -12
- package/src/actions/scales/editPolicy.js +81 -11
- package/src/actions/scales/index.js +6 -0
- package/src/actions/scales/materialize.js +5 -4
- package/src/actions/scales/offset.js +48 -0
- package/src/actions/scales/parallel.js +71 -0
- package/src/actions/scales/preview.js +111 -10
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +179 -66
- package/src/actions/selection/index.js +3 -0
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +70 -15
- package/src/actions/theme/composition.js +238 -0
- package/src/actions/theme/index.js +9 -2
- package/src/actions/theme/reconcile.js +313 -121
- package/src/actions/theme/state.js +160 -0
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +6 -3
- package/src/actions/violinPlots/edit.js +2 -1
- package/src/core/action.js +117 -35
- package/src/core/compositionState.js +6 -1
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +21 -1
- package/src/core/resourceReferences.js +341 -0
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/core/vocabulary.js +16 -3
- package/src/diagnostics.js +1 -0
- package/src/grammar/bars/aggregate.js +4 -1
- package/src/grammar/bars/geometry.js +44 -0
- package/src/grammar/bin.js +54 -9
- package/src/grammar/complete.js +279 -0
- package/src/grammar/computed.js +265 -64
- package/src/grammar/coordinates.js +37 -0
- package/src/grammar/density.js +239 -37
- package/src/grammar/displayLabels.js +57 -0
- package/src/grammar/facets/dependencies.js +133 -1
- package/src/grammar/facets/guides.js +3 -0
- package/src/grammar/facets/index.js +7 -47
- package/src/grammar/facets/scales.js +15 -18
- package/src/grammar/impute.js +341 -0
- package/src/grammar/markLabelSelection.js +57 -0
- package/src/grammar/markLabels.js +18 -1
- package/src/grammar/normalize.js +366 -0
- package/src/grammar/pathSeries.js +26 -8
- package/src/grammar/pointShapes.js +13 -4
- package/src/grammar/polar.js +90 -8
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/parameters.js +23 -0
- package/src/grammar/roundedRect.js +180 -0
- package/src/grammar/rules.js +1 -0
- package/src/grammar/scales/colorConsumers.js +20 -1
- package/src/grammar/scales/continuous.js +2 -2
- package/src/grammar/scales/definition.js +27 -4
- package/src/grammar/scales/index.js +1 -0
- package/src/grammar/scales/ordinal.js +12 -3
- package/src/grammar/scales/policies.js +2 -2
- package/src/grammar/scales/size.js +456 -0
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/concreteGraphic.js +14 -2
- package/src/grammar/schemas/graphic.js +15 -0
- package/src/grammar/schemas/graphicBounds.js +190 -41
- package/src/grammar/schemas/semanticPath.js +7 -2
- package/src/grammar/statisticalReference.js +103 -0
- package/src/grammar/strokeStyle.js +77 -0
- package/src/grammar/summary.js +78 -16
- package/src/grammar/timeUnit.js +216 -5
- package/src/grammar/transformTopology.js +4 -0
- package/src/grammar/transforms.js +394 -28
- package/src/grammar/weightedStatistics.js +374 -0
- package/src/grammar/window.js +221 -16
- package/src/layout/aspect.js +80 -0
- package/src/layout/facets.js +59 -5
- package/src/layout/labels.js +351 -7
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/bars/aggregate.js +25 -2
- package/src/materialization/bars/histogram.js +84 -24
- package/src/materialization/bars/ranged.js +19 -3
- package/src/materialization/bars/resolve.js +7 -4
- package/src/materialization/composition.js +3 -1
- package/src/materialization/coordinateBounds.js +204 -0
- package/src/materialization/dependencies.js +36 -0
- package/src/materialization/facetGuides/legacyCategorical.js +3 -0
- package/src/materialization/facetGuides/placement.js +9 -3
- package/src/materialization/facetGuides/preparation.js +13 -8
- package/src/materialization/facetHeaders.js +347 -0
- package/src/materialization/facets.js +15 -65
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/guides/resources.js +26 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/index.js +11 -5
- package/src/materialization/marks/policies.js +2 -1
- package/src/materialization/rect.js +25 -12
- package/src/materialization/rectConfig.js +5 -1
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/scales/map.js +5 -3
- package/src/materialization/scales/policies/binnedPosition.js +2 -2
- package/src/materialization/scales/policies/offset.js +50 -8
- package/src/materialization/scales/policies/series.js +4 -1
- package/src/materialization/scales/resolve.js +22 -6
- package/src/materialization/selection/items/arc.js +28 -4
- package/src/materialization/selection/items/bar.js +30 -3
- package/src/materialization/selection/items/common.js +20 -5
- package/src/materialization/selection/items/rect.js +28 -4
- package/src/materialization/selection/path.js +1 -1
- package/src/materialization/selection/point.js +3 -1
- package/src/materialization/text.js +265 -14
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +74 -0
- package/src/renderers/canvas/circle.js +2 -3
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/canvas/line.js +2 -3
- package/src/renderers/canvas/native.js +4 -4
- package/src/renderers/canvas/path.js +2 -3
- package/src/renderers/canvas/rect.js +2 -3
- package/src/renderers/canvas/stroke.js +11 -0
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +29 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +38 -5
- package/src/selectors/layers.js +10 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +103 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +10 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +90 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +944 -118
|
@@ -1,10 +1,14 @@
|
|
|
1
|
-
import { action } from "../../../core/action.js";
|
|
1
|
+
import { action, closedAction } from "../../../core/action.js";
|
|
2
2
|
import {
|
|
3
|
-
validateGeneratedItemLimit,
|
|
4
3
|
validateNonEmptyString,
|
|
5
4
|
validateKeys
|
|
6
5
|
} from "../../../core/validation.js";
|
|
7
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
formatDiscretizedIntervals,
|
|
8
|
+
isDiscreteSizeScaleType,
|
|
9
|
+
isSizeScaleType,
|
|
10
|
+
mapSizeValues
|
|
11
|
+
} from "../../../grammar/scales/index.js";
|
|
8
12
|
import { resolveLegendItemLayout } from "../../../layout/legendItems.js";
|
|
9
13
|
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
10
14
|
"../../../theme/defaults.js";
|
|
@@ -13,7 +17,8 @@ import { resolveLegendGraphicPlacement } from
|
|
|
13
17
|
"../../../materialization/graphicHierarchy.js";
|
|
14
18
|
import {
|
|
15
19
|
assertLegendBoundsInsideCanvas,
|
|
16
|
-
|
|
20
|
+
materializeItemLegend,
|
|
21
|
+
createItemLegendGraphics,
|
|
17
22
|
normalizeItemLegendLayout,
|
|
18
23
|
normalizeLegendBorder,
|
|
19
24
|
normalizeLegendTextOptions,
|
|
@@ -21,13 +26,17 @@ import {
|
|
|
21
26
|
resolveContinuousBounds,
|
|
22
27
|
resolveLegendBackgroundFromBounds,
|
|
23
28
|
formatContinuousValues,
|
|
24
|
-
|
|
25
|
-
selectLegendLayer,
|
|
26
|
-
styleContinuousText
|
|
29
|
+
selectLegendLayer
|
|
27
30
|
} from "./continuous/common.js";
|
|
28
31
|
import { legendResourcePolicies } from "../../../materialization/guides/resources.js";
|
|
32
|
+
import {
|
|
33
|
+
normalizeLegendSampling,
|
|
34
|
+
readLegendSampling,
|
|
35
|
+
resolveLegendSampleValues
|
|
36
|
+
} from "./sampling.js";
|
|
37
|
+
import { resolveEffectiveLegendBlockConfig } from "./blocks.js";
|
|
29
38
|
|
|
30
|
-
const SIZE_OPTIONS = Object.freeze(["target", "count", "position", "layout", "align",
|
|
39
|
+
const SIZE_OPTIONS = Object.freeze(["target", "count", "values", "position", "layout", "align",
|
|
31
40
|
"direction", "columns", "titlePosition", "offset", "itemGap", "title", "labels", "titleStyle", "border"]);
|
|
32
41
|
|
|
33
42
|
export const SIZE_LEGEND_LABELS = Object.freeze({
|
|
@@ -61,21 +70,32 @@ export function resolveSizeLegendPoint(program, requested) {
|
|
|
61
70
|
return layer;
|
|
62
71
|
}
|
|
63
72
|
|
|
64
|
-
function requireScale(program, id
|
|
73
|
+
function requireScale(program, id) {
|
|
65
74
|
const scale = program.resolvedScales[id];
|
|
66
|
-
if (scale?.type
|
|
67
|
-
throw new Error(`Legend requires resolved
|
|
75
|
+
if (!isSizeScaleType(scale?.type)) {
|
|
76
|
+
throw new Error(`Legend requires resolved size scale "${id}".`);
|
|
68
77
|
}
|
|
69
78
|
return scale;
|
|
70
79
|
}
|
|
71
80
|
|
|
72
81
|
export function resolveSizeLegendLayout(program, config) {
|
|
73
|
-
const scale = requireScale(program, config.scale
|
|
74
|
-
const categorical = [
|
|
82
|
+
const scale = requireScale(program, config.scale);
|
|
83
|
+
const categorical = [
|
|
84
|
+
program.guideConfigs.legend?.series,
|
|
85
|
+
program.guideConfigs.legend?.color,
|
|
86
|
+
program.guideConfigs.legend?.stroke
|
|
87
|
+
]
|
|
75
88
|
.find(candidate => candidate?.target === config.target);
|
|
76
89
|
const inherit = config.inheritAppearance === true && categorical !== undefined;
|
|
77
|
-
const
|
|
78
|
-
const
|
|
90
|
+
const inheritedLabels = inherit ? { ...categorical.labels, offset: config.labels.offset } : config.labels;
|
|
91
|
+
const inheritedTitleStyle = inherit ? categorical.titleStyle : config.titleStyle;
|
|
92
|
+
const effective = resolveEffectiveLegendBlockConfig(program, "size", {
|
|
93
|
+
...config,
|
|
94
|
+
labels: inheritedLabels,
|
|
95
|
+
titleStyle: inheritedTitleStyle
|
|
96
|
+
});
|
|
97
|
+
const labels = effective.labels;
|
|
98
|
+
const titleStyle = effective.titleStyle;
|
|
79
99
|
const position = categorical?.position ?? config.position;
|
|
80
100
|
if (categorical?.layout === "legacy-bottom") {
|
|
81
101
|
throw new Error('Combined size legends require layout "edge".');
|
|
@@ -84,36 +104,54 @@ export function resolveSizeLegendLayout(program, config) {
|
|
|
84
104
|
const geometry = horizontal ? {
|
|
85
105
|
position, align: categorical.align, direction: categorical.direction,
|
|
86
106
|
columns: categorical.columns, titlePosition: categorical.titlePosition,
|
|
87
|
-
offset: categorical.offset, itemGap: categorical.itemGap
|
|
107
|
+
offset: categorical.offset, itemGap: effective.blockGap ?? categorical.itemGap
|
|
88
108
|
} : { position };
|
|
89
|
-
const
|
|
90
|
-
|
|
109
|
+
const discrete = isDiscreteSizeScaleType(scale.type);
|
|
110
|
+
if (discrete && readLegendSampling(config).mode === "values") {
|
|
111
|
+
throw new Error("Discrete size legends do not support exact values.");
|
|
112
|
+
}
|
|
113
|
+
const values = discrete
|
|
114
|
+
? undefined
|
|
115
|
+
: resolveLegendSampleValues(effective, scale, "Size legend");
|
|
116
|
+
const areas = discrete
|
|
117
|
+
? scale.range
|
|
118
|
+
: mapSizeValues(values, scale);
|
|
91
119
|
const radii = areas.map(area => Math.sqrt(area / Math.PI));
|
|
92
120
|
const radius = Math.max(...radii);
|
|
93
121
|
const width = Math.max(32, radius * 2);
|
|
94
122
|
const { plot, canvas } = resolveContinuousBounds(program);
|
|
95
|
-
const text =
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
123
|
+
const text = discrete
|
|
124
|
+
? formatDiscretizedIntervals(scale.thresholds, config.labels.format)
|
|
125
|
+
: formatContinuousValues(
|
|
126
|
+
values,
|
|
127
|
+
scale.domain,
|
|
128
|
+
"quantitative",
|
|
129
|
+
config.labels.format
|
|
130
|
+
);
|
|
131
|
+
const symbolStroke = effective.blockSymbol?.strokeWidth ?? 0;
|
|
132
|
+
const symbolExtent = symbolStroke / 2;
|
|
133
|
+
const sampleWidth = width + symbolStroke;
|
|
134
|
+
const layout = resolveLegendItemLayout(plot, { ...effective, ...geometry, labels, titleStyle }, text, {
|
|
135
|
+
width: sampleWidth, height: radius * 2 + symbolStroke,
|
|
136
|
+
itemBounds: radii.map(r => ({
|
|
137
|
+
left: sampleWidth / 2 - r - symbolExtent,
|
|
138
|
+
right: sampleWidth / 2 + r + symbolExtent,
|
|
139
|
+
top: -r - symbolExtent,
|
|
140
|
+
bottom: r + symbolExtent
|
|
141
|
+
}))
|
|
142
|
+
}, undefined, program.materializationConfigs.textMetrics);
|
|
143
|
+
assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Size legend layout", { ...effective, ...geometry });
|
|
144
|
+
const background = resolveLegendBackgroundFromBounds(layout.bounds, effective.border, canvas, "Size legend", { ...effective, ...geometry });
|
|
145
|
+
return { ...layout, symbolX: layout.symbolX.map(x => x + sampleWidth / 2), radii, text,
|
|
146
|
+
labels, titleStyle, background, config: effective };
|
|
108
147
|
}
|
|
109
148
|
|
|
110
|
-
export const rematerializeSizeLegend =
|
|
149
|
+
export const rematerializeSizeLegend = /* @__PURE__ */ closedAction(
|
|
111
150
|
{
|
|
112
151
|
op: "rematerializeSizeLegend",
|
|
113
152
|
description: "Rematerialize a quantitative point-size legend."
|
|
114
|
-
},
|
|
153
|
+
}, [],
|
|
115
154
|
function (args = {}) {
|
|
116
|
-
validateKeys(args, [], "rematerializeSizeLegend");
|
|
117
155
|
const config = this.guideConfigs.legend?.size;
|
|
118
156
|
if (config === undefined) throw new Error("Size legend requires stored configuration.");
|
|
119
157
|
const layer = findLayer(this, config.target);
|
|
@@ -121,7 +159,7 @@ export const rematerializeSizeLegend = action(
|
|
|
121
159
|
if (encoding?.scale === undefined) {
|
|
122
160
|
throw new Error("Size legend target requires a size encoding.");
|
|
123
161
|
}
|
|
124
|
-
const scale = requireScale(this, encoding.scale
|
|
162
|
+
const scale = requireScale(this, encoding.scale);
|
|
125
163
|
const title = config.inferredTitle === true ? encoding.field : config.title;
|
|
126
164
|
const currentConfig = {
|
|
127
165
|
...config,
|
|
@@ -130,41 +168,18 @@ export const rematerializeSizeLegend = action(
|
|
|
130
168
|
domain: scale.domain
|
|
131
169
|
};
|
|
132
170
|
const layout = resolveSizeLegendLayout(this, currentConfig);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
})
|
|
146
|
-
.editGraphics({
|
|
147
|
-
target: "sizeLegendSymbols",
|
|
148
|
-
property: "fill",
|
|
149
|
-
value: DEFAULT_COLORS.sizeSymbol
|
|
150
|
-
})
|
|
151
|
-
.editGraphics({ target: "sizeLegendSymbols", property: "opacity", value: 0.7 })
|
|
152
|
-
.editGraphics({ target: "sizeLegendLabels", property: "length", value: radii.length })
|
|
153
|
-
.editGraphics({ target: "sizeLegendLabels", property: "x", value: labelX })
|
|
154
|
-
.editGraphics({ target: "sizeLegendLabels", property: "y", value: itemY })
|
|
155
|
-
.editGraphics({
|
|
156
|
-
target: "sizeLegendLabels",
|
|
157
|
-
property: "text",
|
|
158
|
-
value: layout.text
|
|
159
|
-
});
|
|
160
|
-
next = editLegendBackground(next, "sizeLegendBackground", layout.background, currentConfig.border);
|
|
161
|
-
next = styleContinuousText(next, "sizeLegendLabels", labels);
|
|
162
|
-
if (currentConfig.titleVisible === false) return next;
|
|
163
|
-
next = next
|
|
164
|
-
.editGraphics({ target: "sizeLegendTitle", property: "x", value: layout.title.x })
|
|
165
|
-
.editGraphics({ target: "sizeLegendTitle", property: "y", value: layout.title.y })
|
|
166
|
-
.editGraphics({ target: "sizeLegendTitle", property: "text", value: title });
|
|
167
|
-
return styleContinuousText(next, "sizeLegendTitle", titleStyle, { align: layout.title.align });
|
|
171
|
+
return materializeItemLegend(this, "size", currentConfig, layout, {
|
|
172
|
+
text: layout.text, labels: layout.labels, titleStyle: layout.titleStyle,
|
|
173
|
+
symbols: {
|
|
174
|
+
x: layout.symbolX, y: layout.itemY, radius: layout.radii,
|
|
175
|
+
fill: layout.config.blockSymbol?.fill ?? DEFAULT_COLORS.sizeSymbol,
|
|
176
|
+
opacity: layout.config.blockSymbol?.opacity ?? 0.7
|
|
177
|
+
},
|
|
178
|
+
optional: {
|
|
179
|
+
stroke: layout.config.blockSymbol?.stroke,
|
|
180
|
+
strokeWidth: layout.config.blockSymbol?.strokeWidth
|
|
181
|
+
}
|
|
182
|
+
});
|
|
168
183
|
}
|
|
169
184
|
);
|
|
170
185
|
|
|
@@ -177,12 +192,16 @@ export function resolveSizeLegendConfig(program, args = {}) {
|
|
|
177
192
|
if (encoding?.scale === undefined) {
|
|
178
193
|
throw new Error(`Point mark "${layer.id}" requires a size encoding.`);
|
|
179
194
|
}
|
|
180
|
-
const scale = requireScale(program, encoding.scale
|
|
181
|
-
const
|
|
182
|
-
if (
|
|
183
|
-
throw new
|
|
195
|
+
const scale = requireScale(program, encoding.scale);
|
|
196
|
+
const discrete = isDiscreteSizeScaleType(scale.type);
|
|
197
|
+
if (discrete && (Object.hasOwn(args, "count") || Object.hasOwn(args, "values"))) {
|
|
198
|
+
throw new Error("Discrete size legends do not support count or exact values.");
|
|
184
199
|
}
|
|
185
|
-
|
|
200
|
+
const sampling = discrete ? undefined : normalizeLegendSampling(args, {
|
|
201
|
+
operation: "create",
|
|
202
|
+
label: "Size legend"
|
|
203
|
+
});
|
|
204
|
+
const count = discrete ? scale.range.length : undefined;
|
|
186
205
|
return {
|
|
187
206
|
target: layer.id,
|
|
188
207
|
scale: encoding.scale,
|
|
@@ -190,7 +209,7 @@ export function resolveSizeLegendConfig(program, args = {}) {
|
|
|
190
209
|
title: args.title ?? encoding.field,
|
|
191
210
|
inferredTitle: args.title === undefined,
|
|
192
211
|
domain: scale.domain,
|
|
193
|
-
count,
|
|
212
|
+
...(discrete ? { count } : { sampling }),
|
|
194
213
|
inheritAppearance: args.inheritAppearance === true,
|
|
195
214
|
labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", SIZE_LEGEND_LABELS),
|
|
196
215
|
titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", SIZE_LEGEND_TITLE_STYLE),
|
|
@@ -200,48 +219,21 @@ export function resolveSizeLegendConfig(program, args = {}) {
|
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
export function createSizeLegendFromConfig(program, config) {
|
|
203
|
-
resolveSizeLegendLayout(program, config);
|
|
204
|
-
const
|
|
222
|
+
const { radii } = resolveSizeLegendLayout(program, config);
|
|
223
|
+
const count = radii.length;
|
|
205
224
|
const following = new Set(legendResourcePolicies().filter(policy =>
|
|
206
225
|
policy.kind !== "size" && policy.family !== "categorical" &&
|
|
207
226
|
program.guideConfigs.legend?.[policy.kind] !== undefined
|
|
208
227
|
).flatMap(policy => policy.graphicIds));
|
|
209
228
|
const before = program.graphicSpec.objects.canvas?.children?.find(id => following.has(id));
|
|
210
229
|
const placement = resolveLegendGraphicPlacement(program, before === undefined ? {} : { before });
|
|
211
|
-
|
|
212
|
-
.editSemantic({ property: "guide.legend.size.scale", value: config.scale })
|
|
213
|
-
.editSemantic({ property: "guide.legend.size.title", value: config.title })
|
|
214
|
-
._withLegendConfig("size", config);
|
|
215
|
-
if (config.border !== false) {
|
|
216
|
-
next = next.createGraphics({ id: "sizeLegendBackground", type: "rect",
|
|
217
|
-
...placement });
|
|
218
|
-
}
|
|
219
|
-
next = next.createGraphics({
|
|
220
|
-
id: "sizeLegendSymbols",
|
|
221
|
-
type: "circle",
|
|
222
|
-
length: count,
|
|
223
|
-
...placement
|
|
224
|
-
})
|
|
225
|
-
.createGraphics({
|
|
226
|
-
id: "sizeLegendLabels",
|
|
227
|
-
type: "text",
|
|
228
|
-
length: count,
|
|
229
|
-
...placement
|
|
230
|
-
});
|
|
231
|
-
if (config.titleVisible !== false) {
|
|
232
|
-
next = next.createGraphics({
|
|
233
|
-
id: "sizeLegendTitle",
|
|
234
|
-
type: "text",
|
|
235
|
-
...placement
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
return next.rematerializeSizeLegend();
|
|
230
|
+
return createItemLegendGraphics(program, "size", config, count, "circle", placement).rematerializeSizeLegend();
|
|
239
231
|
}
|
|
240
232
|
|
|
241
|
-
export const createSizeLegend = action(
|
|
233
|
+
export const createSizeLegend = /* @__PURE__ */ action(
|
|
242
234
|
{
|
|
243
235
|
op: "createSizeLegend",
|
|
244
|
-
description: "Create
|
|
236
|
+
description: "Create an equal-area point-size legend."
|
|
245
237
|
},
|
|
246
238
|
function (args = {}) {
|
|
247
239
|
const config = resolveSizeLegendConfig(this, args);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { action } from "../../../core/action.js";
|
|
1
|
+
import { action, closedAction } from "../../../core/action.js";
|
|
2
2
|
import {
|
|
3
|
-
validateGeneratedItemLimit,
|
|
4
3
|
validateNonEmptyString,
|
|
5
4
|
validateKeys
|
|
6
5
|
} from "../../../core/validation.js";
|
|
@@ -13,7 +12,8 @@ import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
|
13
12
|
"../../../theme/defaults.js";
|
|
14
13
|
import {
|
|
15
14
|
assertLegendBoundsInsideCanvas,
|
|
16
|
-
|
|
15
|
+
materializeItemLegend,
|
|
16
|
+
createItemLegendGraphics,
|
|
17
17
|
normalizeItemLegendLayout,
|
|
18
18
|
normalizeLegendBorder,
|
|
19
19
|
normalizeLegendTextOptions,
|
|
@@ -21,12 +21,15 @@ import {
|
|
|
21
21
|
resolveContinuousBounds,
|
|
22
22
|
resolveLegendBackgroundFromBounds,
|
|
23
23
|
formatContinuousValues,
|
|
24
|
-
|
|
25
|
-
selectLegendLayer,
|
|
26
|
-
styleContinuousText
|
|
24
|
+
selectLegendLayer
|
|
27
25
|
} from "./continuous/common.js";
|
|
26
|
+
import {
|
|
27
|
+
normalizeLegendSampling,
|
|
28
|
+
resolveLegendSampleValues
|
|
29
|
+
} from "./sampling.js";
|
|
30
|
+
import { resolveEffectiveLegendBlockConfig } from "./blocks.js";
|
|
28
31
|
|
|
29
|
-
const OPTIONS = Object.freeze(["target", "count", "position", "layout", "align",
|
|
32
|
+
const OPTIONS = Object.freeze(["target", "count", "values", "position", "layout", "align",
|
|
30
33
|
"direction", "columns", "titlePosition", "offset", "itemGap", "title", "labels", "titleStyle", "border"]);
|
|
31
34
|
|
|
32
35
|
export const STROKE_WIDTH_LEGEND_LABELS = Object.freeze({
|
|
@@ -74,29 +77,29 @@ function requireScale(program, id) {
|
|
|
74
77
|
}
|
|
75
78
|
|
|
76
79
|
export function resolveStrokeWidthLegendLayout(program, config) {
|
|
80
|
+
const effective = resolveEffectiveLegendBlockConfig(program, "strokeWidth", config);
|
|
77
81
|
const scale = requireScale(program, config.scale);
|
|
78
82
|
const { plot, canvas } = resolveContinuousBounds(program);
|
|
79
|
-
const values =
|
|
83
|
+
const values = resolveLegendSampleValues(effective, scale, "Stroke-width legend");
|
|
80
84
|
const widths = mapContinuousScaleValues(values, scale);
|
|
81
85
|
const labels = formatContinuousValues(
|
|
82
86
|
values,
|
|
83
87
|
scale.domain,
|
|
84
88
|
"quantitative",
|
|
85
|
-
|
|
89
|
+
effective.labels.format
|
|
86
90
|
);
|
|
87
|
-
const layout = resolveLegendItemLayout(plot,
|
|
88
|
-
assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Stroke-width legend layout",
|
|
89
|
-
const background = resolveLegendBackgroundFromBounds(layout.bounds,
|
|
90
|
-
return { ...layout, widths, labels, background };
|
|
91
|
+
const layout = resolveLegendItemLayout(plot, effective, labels, { width: 32, height: 0, strokeWidth: widths }, undefined, program.materializationConfigs.textMetrics);
|
|
92
|
+
assertLegendBoundsInsideCanvas(layout.bounds, canvas, "Stroke-width legend layout", effective);
|
|
93
|
+
const background = resolveLegendBackgroundFromBounds(layout.bounds, effective.border, canvas, "Stroke-width legend", effective);
|
|
94
|
+
return { ...layout, widths, labels, background, config: effective };
|
|
91
95
|
}
|
|
92
96
|
|
|
93
|
-
export const rematerializeStrokeWidthLegend = /* @__PURE__ */
|
|
97
|
+
export const rematerializeStrokeWidthLegend = /* @__PURE__ */ closedAction(
|
|
94
98
|
{
|
|
95
99
|
op: "rematerializeStrokeWidthLegend",
|
|
96
100
|
description: "Rematerialize a quantitative stroke-width legend."
|
|
97
|
-
},
|
|
101
|
+
}, [],
|
|
98
102
|
function (args = {}) {
|
|
99
|
-
validateKeys(args, [], "rematerializeStrokeWidthLegend");
|
|
100
103
|
const stored = this.guideConfigs.legend?.strokeWidth;
|
|
101
104
|
if (stored === undefined) {
|
|
102
105
|
throw new Error("Stroke-width legend requires stored configuration.");
|
|
@@ -114,44 +117,17 @@ export const rematerializeStrokeWidthLegend = /* @__PURE__ */ action(
|
|
|
114
117
|
}
|
|
115
118
|
const scale = requireScale(this, encoding.scale);
|
|
116
119
|
const title = config.inferredTitle === true ? encoding.field : config.title;
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "length", value: widths.length })
|
|
129
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "x1", value: layout.symbolX })
|
|
130
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "x2", value: layout.symbolX.map(x => x + 32) })
|
|
131
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "y1", value: y })
|
|
132
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "y2", value: y })
|
|
133
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "stroke", value: DEFAULT_COLORS.mark })
|
|
134
|
-
.editGraphics({ target: "strokeWidthLegendSymbols", property: "strokeWidth", value: widths })
|
|
135
|
-
.editGraphics({ target: "strokeWidthLegendLabels", property: "length", value: widths.length })
|
|
136
|
-
.editGraphics({
|
|
137
|
-
target: "strokeWidthLegendLabels",
|
|
138
|
-
property: "x",
|
|
139
|
-
value: layout.labelX
|
|
140
|
-
})
|
|
141
|
-
.editGraphics({ target: "strokeWidthLegendLabels", property: "y", value: y })
|
|
142
|
-
.editGraphics({
|
|
143
|
-
target: "strokeWidthLegendLabels",
|
|
144
|
-
property: "text",
|
|
145
|
-
value: layout.labels
|
|
146
|
-
});
|
|
147
|
-
next = editLegendBackground(next, "strokeWidthLegendBackground", layout.background, config.border);
|
|
148
|
-
next = styleContinuousText(next, "strokeWidthLegendLabels", config.labels);
|
|
149
|
-
if (config.titleVisible === false) return next;
|
|
150
|
-
next = next
|
|
151
|
-
.editGraphics({ target: "strokeWidthLegendTitle", property: "x", value: layout.title.x })
|
|
152
|
-
.editGraphics({ target: "strokeWidthLegendTitle", property: "y", value: layout.title.y })
|
|
153
|
-
.editGraphics({ target: "strokeWidthLegendTitle", property: "text", value: title });
|
|
154
|
-
return styleContinuousText(next, "strokeWidthLegendTitle", config.titleStyle, { align: layout.title.align });
|
|
120
|
+
const currentConfig = { ...config, scale: encoding.scale, title };
|
|
121
|
+
const layout = resolveStrokeWidthLegendLayout(this, currentConfig);
|
|
122
|
+
return materializeItemLegend(this, "strokeWidth", { ...currentConfig, domain: scale.domain }, layout, {
|
|
123
|
+
symbols: {
|
|
124
|
+
x1: layout.symbolX, x2: layout.symbolX.map(x => x + 32),
|
|
125
|
+
y1: layout.itemY, y2: layout.itemY,
|
|
126
|
+
stroke: layout.config.blockSymbol?.stroke ?? DEFAULT_COLORS.mark,
|
|
127
|
+
strokeWidth: layout.widths
|
|
128
|
+
},
|
|
129
|
+
optional: { opacity: layout.config.blockSymbol?.opacity }
|
|
130
|
+
});
|
|
155
131
|
}
|
|
156
132
|
);
|
|
157
133
|
|
|
@@ -162,20 +138,17 @@ export function resolveStrokeWidthLegendConfig(program, args = {}) {
|
|
|
162
138
|
const layer = resolveLayer(program, args.target);
|
|
163
139
|
const encoding = layer.encoding.strokeWidth;
|
|
164
140
|
requireScale(program, encoding.scale);
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
validateGeneratedItemLimit(count, "Stroke-width legend count");
|
|
141
|
+
const sampling = normalizeLegendSampling(args, {
|
|
142
|
+
operation: "create",
|
|
143
|
+
label: "Stroke-width legend"
|
|
144
|
+
});
|
|
172
145
|
return {
|
|
173
146
|
target: layer.id,
|
|
174
147
|
scale: encoding.scale,
|
|
175
148
|
...normalizeItemLegendLayout({ ...args, itemGap: args.itemGap ?? 32 }),
|
|
176
149
|
title: args.title ?? encoding.field,
|
|
177
150
|
inferredTitle: args.title === undefined,
|
|
178
|
-
|
|
151
|
+
sampling,
|
|
179
152
|
labels: normalizeLegendTextOptions(args.labels, "createLegend.labels", STROKE_WIDTH_LEGEND_LABELS),
|
|
180
153
|
titleStyle: normalizeLegendTitleOptions(args.titleStyle, "createLegend.titleStyle", STROKE_WIDTH_LEGEND_TITLE_STYLE),
|
|
181
154
|
border: normalizeLegendBorder(args.border),
|
|
@@ -184,36 +157,9 @@ export function resolveStrokeWidthLegendConfig(program, args = {}) {
|
|
|
184
157
|
}
|
|
185
158
|
|
|
186
159
|
export function createStrokeWidthLegendFromConfig(program, config) {
|
|
187
|
-
resolveStrokeWidthLegendLayout(program, config);
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
.editSemantic({ property: "guide.legend.strokeWidth.scale", value: config.scale })
|
|
191
|
-
.editSemantic({ property: "guide.legend.strokeWidth.title", value: config.title })
|
|
192
|
-
._withLegendConfig("strokeWidth", config);
|
|
193
|
-
if (config.border !== false) {
|
|
194
|
-
next = next.createGraphics({ id: "strokeWidthLegendBackground", type: "rect",
|
|
195
|
-
...resolveLegendGraphicPlacement(program) });
|
|
196
|
-
}
|
|
197
|
-
next = next.createGraphics({
|
|
198
|
-
id: "strokeWidthLegendSymbols",
|
|
199
|
-
type: "line",
|
|
200
|
-
length: count,
|
|
201
|
-
...resolveLegendGraphicPlacement(program)
|
|
202
|
-
})
|
|
203
|
-
.createGraphics({
|
|
204
|
-
id: "strokeWidthLegendLabels",
|
|
205
|
-
type: "text",
|
|
206
|
-
length: count,
|
|
207
|
-
...resolveLegendGraphicPlacement(program)
|
|
208
|
-
});
|
|
209
|
-
if (config.titleVisible !== false) {
|
|
210
|
-
next = next.createGraphics({
|
|
211
|
-
id: "strokeWidthLegendTitle",
|
|
212
|
-
type: "text",
|
|
213
|
-
...resolveLegendGraphicPlacement(program)
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
return next.rematerializeStrokeWidthLegend();
|
|
160
|
+
const layout = resolveStrokeWidthLegendLayout(program, config);
|
|
161
|
+
const count = layout.widths.length;
|
|
162
|
+
return createItemLegendGraphics(program, "strokeWidth", config, count, "line", resolveLegendGraphicPlacement(program)).rematerializeStrokeWidthLegend();
|
|
217
163
|
}
|
|
218
164
|
|
|
219
165
|
export const createStrokeWidthLegend = /* @__PURE__ */ action(
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { validateUserId } from "../../../core/identifiers.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
LEGEND_CHANNELS,
|
|
4
|
+
LEGEND_CONFIG_KINDS
|
|
5
|
+
} from "../../../core/vocabulary.js";
|
|
6
|
+
import { isDiscreteSizeScaleType } from "../../../grammar/scales/index.js";
|
|
3
7
|
|
|
4
8
|
export function validateLegendChannels(channels, operation) {
|
|
5
9
|
if (!Array.isArray(channels)) throw new TypeError(`${operation} channels must be an array.`);
|
|
@@ -35,3 +39,110 @@ export function resolveLegendTarget(program, requested, operation) {
|
|
|
35
39
|
}
|
|
36
40
|
return targets[0];
|
|
37
41
|
}
|
|
42
|
+
|
|
43
|
+
const CATEGORICAL_KINDS = Object.freeze(["series", "color", "stroke"]);
|
|
44
|
+
const SINGLE_CHANNELS = Object.freeze({
|
|
45
|
+
size: "size",
|
|
46
|
+
opacity: "opacity",
|
|
47
|
+
strokeWidth: "strokeWidth",
|
|
48
|
+
gradient: "color",
|
|
49
|
+
interval: "color",
|
|
50
|
+
strokeGradient: "stroke",
|
|
51
|
+
strokeInterval: "stroke"
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const asciiCompare = (left, right) => left < right ? -1 : left > right ? 1 : 0;
|
|
55
|
+
|
|
56
|
+
export function legendBlockChannels(kind, config) {
|
|
57
|
+
const channels = CATEGORICAL_KINDS.includes(kind)
|
|
58
|
+
? config?.channels
|
|
59
|
+
: SINGLE_CHANNELS[kind] === undefined
|
|
60
|
+
? undefined
|
|
61
|
+
: [SINGLE_CHANNELS[kind]];
|
|
62
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
63
|
+
throw new Error(`Legend block ${kind} requires at least one channel.`);
|
|
64
|
+
}
|
|
65
|
+
validateLegendChannels(channels, `Legend block ${kind}`);
|
|
66
|
+
return Object.freeze([...channels].sort(asciiCompare));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function legendBlockKey(channels) {
|
|
70
|
+
if (!Array.isArray(channels) || channels.length === 0) {
|
|
71
|
+
throw new TypeError("Legend block key requires a non-empty channel array.");
|
|
72
|
+
}
|
|
73
|
+
return JSON.stringify([...channels].sort(asciiCompare));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function describeLegendBlock(program, kind, config) {
|
|
77
|
+
if (!LEGEND_CONFIG_KINDS.includes(kind)) {
|
|
78
|
+
throw new Error(`Unknown legend block kind "${kind}".`);
|
|
79
|
+
}
|
|
80
|
+
const channels = legendBlockChannels(kind, config);
|
|
81
|
+
const scaleIds = CATEGORICAL_KINDS.includes(kind)
|
|
82
|
+
? config?.scales
|
|
83
|
+
: [config?.scale];
|
|
84
|
+
if (!Array.isArray(scaleIds) || scaleIds.length === 0 ||
|
|
85
|
+
scaleIds.some(id => typeof id !== "string" || program.resolvedScales[id] === undefined)) {
|
|
86
|
+
throw new Error(`Legend block ${kind} requires resolved scales.`);
|
|
87
|
+
}
|
|
88
|
+
const family = CATEGORICAL_KINDS.includes(kind)
|
|
89
|
+
? "categorical"
|
|
90
|
+
: kind === "size"
|
|
91
|
+
? isDiscreteSizeScaleType(program.resolvedScales[scaleIds[0]].type)
|
|
92
|
+
? "discrete-size"
|
|
93
|
+
: "sampled"
|
|
94
|
+
: ["opacity", "strokeWidth"].includes(kind)
|
|
95
|
+
? "sampled"
|
|
96
|
+
: ["gradient", "strokeGradient"].includes(kind)
|
|
97
|
+
? "gradient"
|
|
98
|
+
: "interval";
|
|
99
|
+
return Object.freeze({
|
|
100
|
+
target: config.target,
|
|
101
|
+
kind,
|
|
102
|
+
family,
|
|
103
|
+
key: legendBlockKey(channels),
|
|
104
|
+
channels,
|
|
105
|
+
scaleIds: Object.freeze([...scaleIds]),
|
|
106
|
+
config
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function legendBlockDescriptors(program, target) {
|
|
111
|
+
const requested = validateUserId(target, "Legend target id");
|
|
112
|
+
const descriptors = LEGEND_CONFIG_KINDS.flatMap(kind => {
|
|
113
|
+
const config = program.guideConfigs.legend?.[kind];
|
|
114
|
+
return config?.target === requested
|
|
115
|
+
? [describeLegendBlock(program, kind, config)]
|
|
116
|
+
: [];
|
|
117
|
+
});
|
|
118
|
+
const seen = new Set();
|
|
119
|
+
for (const descriptor of descriptors) {
|
|
120
|
+
if (seen.has(descriptor.key)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Legend target "${requested}" has ambiguous block key ${descriptor.key}.`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
seen.add(descriptor.key);
|
|
126
|
+
}
|
|
127
|
+
return Object.freeze(descriptors);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function resolveLegendBlock(program, { target, channel }, operation = "editLegendBlock") {
|
|
131
|
+
const requested = resolveLegendTarget(program, target, operation);
|
|
132
|
+
if (!LEGEND_CHANNELS.includes(channel)) {
|
|
133
|
+
throw new Error(`${operation} has unsupported legend channel "${channel}".`);
|
|
134
|
+
}
|
|
135
|
+
const matches = legendBlockDescriptors(program, requested)
|
|
136
|
+
.filter(descriptor => descriptor.channels.includes(channel));
|
|
137
|
+
if (matches.length === 0) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`${operation} target "${requested}" has no legend block for channel "${channel}".`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
if (matches.length !== 1) {
|
|
143
|
+
throw new Error(
|
|
144
|
+
`${operation} target "${requested}" has ambiguous channel "${channel}".`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return matches[0];
|
|
148
|
+
}
|