ggaction 0.0.12 → 0.0.13
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 +49 -0
- package/README.md +2 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/action-cards.schema.json +1 -1
- package/knowledge/extension-authoring.md +2 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +4 -2
- package/src/actions/basic.js +2 -0
- package/src/actions/boxPlots/materialize.js +2 -1
- package/src/actions/canvas/actions.js +3 -1
- package/src/actions/canvas/fitting.js +211 -0
- package/src/actions/canvas/index.js +2 -0
- package/src/actions/categoryOrder/index.js +3 -2
- package/src/actions/charts/area.js +47 -0
- package/src/actions/charts/bar.js +11 -5
- package/src/actions/charts/beeswarm.js +70 -0
- package/src/actions/charts/density.js +44 -0
- package/src/actions/charts/ecdf.js +183 -0
- package/src/actions/charts/endpoints.js +451 -0
- package/src/actions/charts/heatmap.js +2 -2
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +71 -0
- package/src/actions/charts/index.js +37 -0
- package/src/actions/charts/interval-regression.js +169 -0
- package/src/actions/charts/line.js +5 -5
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +44 -0
- package/src/actions/charts/polar.js +123 -0
- package/src/actions/charts/radar.js +299 -0
- package/src/actions/charts/radial.js +42 -0
- package/src/actions/charts/raincloud.js +422 -0
- package/src/actions/charts/rug-strip.js +279 -0
- package/src/actions/charts/scatter.js +9 -3
- package/src/actions/charts/shared.js +69 -10
- package/src/actions/composition/actions.js +118 -1
- package/src/actions/data/bin.js +29 -0
- package/src/actions/data/computed.js +24 -0
- package/src/actions/data/derived.js +66 -0
- package/src/actions/data/ecdf.js +38 -0
- package/src/actions/data/filter.js +212 -28
- package/src/actions/data/fold.js +24 -0
- package/src/actions/data/index.js +24 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/intervalEdit.js +86 -4
- package/src/actions/data/regression.js +5 -2
- package/src/actions/data/stack.js +26 -0
- package/src/actions/data/summary.js +26 -0
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/appearance.js +32 -13
- package/src/actions/encodings/barWidth.js +8 -0
- package/src/actions/encodings/basic.js +2 -0
- package/src/actions/encodings/color/continuous.js +19 -59
- package/src/actions/encodings/color/index.js +28 -22
- package/src/actions/encodings/color/layout.js +3 -97
- package/src/actions/encodings/color/policy.js +5 -8
- package/src/actions/encodings/density/resolve.js +37 -5
- package/src/actions/encodings/density.js +120 -33
- package/src/actions/encodings/horizon/resolve.js +8 -4
- package/src/actions/encodings/horizon.js +18 -8
- package/src/actions/encodings/index.js +2 -0
- package/src/actions/encodings/offset.js +10 -16
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +10 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +14 -0
- package/src/actions/encodings/position/policies/area.js +4 -42
- package/src/actions/encodings/position/policies/bar.js +48 -13
- package/src/actions/encodings/position/resolve.js +67 -22
- package/src/actions/encodings/ranged.js +123 -97
- package/src/actions/encodings/remove.js +37 -32
- package/src/actions/encodings/ruleAppearance.js +23 -9
- package/src/actions/encodings/seriesLayout.js +104 -0
- package/src/actions/encodings/shared.js +25 -23
- package/src/actions/encodings/strokeDash.js +9 -6
- package/src/actions/encodings/temporal.js +7 -0
- package/src/actions/encodings/text.js +30 -15
- package/src/actions/errorBands/create.js +18 -5
- package/src/actions/errorBands/edit.js +341 -21
- package/src/actions/errorBars/create.js +21 -10
- package/src/actions/errorBars/edit.js +355 -19
- package/src/actions/facets/actions.js +410 -12
- package/src/actions/facets/derive.js +57 -8
- package/src/actions/gradientPlots/components.js +33 -11
- package/src/actions/gradientPlots/materialize.js +5 -7
- package/src/actions/guides/applicability.js +20 -17
- package/src/actions/guides/axes/axes.js +4 -3
- package/src/actions/guides/axes/axis.js +18 -18
- package/src/actions/guides/axes/components.js +43 -0
- package/src/actions/guides/axes/edit.js +16 -51
- package/src/actions/guides/axes/labels.js +129 -19
- package/src/actions/guides/axes/lines.js +7 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +117 -0
- package/src/actions/guides/axes/parallel/policy.js +137 -0
- package/src/actions/guides/axes/parallel/resolve.js +44 -23
- package/src/actions/guides/axes/parallel.js +93 -217
- package/src/actions/guides/axes/policy.js +21 -67
- package/src/actions/guides/axes/tickGroups.js +14 -8
- package/src/actions/guides/axes/ticks.js +5 -4
- package/src/actions/guides/axes/titles.js +21 -8
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facade.js +106 -0
- package/src/actions/guides/facadeAxes.js +200 -0
- package/src/actions/guides/facadeLegend.js +63 -0
- package/src/actions/guides/grids/grid.js +37 -33
- package/src/actions/guides/grids/resolve.js +10 -2
- package/src/actions/guides/guides.js +35 -28
- package/src/actions/guides/legends/categorical/actions.js +193 -249
- package/src/actions/guides/legends/categorical/components.js +4 -5
- package/src/actions/guides/legends/categorical/layout.js +36 -349
- package/src/actions/guides/legends/categorical/options.js +30 -14
- package/src/actions/guides/legends/categorical/recipes.js +14 -0
- package/src/actions/guides/legends/categorical/resolve.js +21 -10
- package/src/actions/guides/legends/categorical/symbols.js +2 -3
- package/src/actions/guides/legends/continuous/common.js +69 -14
- package/src/actions/guides/legends/continuous/gradient.js +111 -83
- package/src/actions/guides/legends/continuous/index.js +6 -2
- package/src/actions/guides/legends/continuous/interval.js +71 -92
- package/src/actions/guides/legends/continuous/opacity.js +105 -84
- package/src/actions/guides/legends/creation.js +27 -0
- package/src/actions/guides/legends/edit.js +198 -66
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/lane.js +98 -86
- package/src/actions/guides/legends/lifecycle.js +132 -0
- package/src/actions/guides/legends/remove.js +31 -76
- package/src/actions/guides/legends/size.js +149 -105
- package/src/actions/guides/legends/strokeWidth.js +106 -64
- package/src/actions/guides/legends/target.js +12 -0
- package/src/actions/guides/legends/transition.js +43 -0
- package/src/actions/guides/polar/axes/facade.js +77 -62
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/lines.js +2 -1
- package/src/actions/guides/polar/axes/shared.js +6 -0
- package/src/actions/guides/polar/axes/ticks.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/grids.js +7 -7
- package/src/actions/guides/reuse.js +44 -0
- package/src/actions/guides/tickValues.js +19 -21
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/resolve.js +35 -15
- package/src/actions/marks/arc/actions.js +1 -1
- package/src/actions/marks/area/actions.js +18 -4
- package/src/actions/marks/area/materialize.js +5 -2
- package/src/actions/marks/bar/materialize.js +11 -7
- package/src/actions/marks/index.js +5 -0
- package/src/actions/marks/line/actions.js +43 -108
- package/src/actions/marks/line/materialize.js +34 -52
- package/src/actions/marks/point/edit.js +11 -15
- package/src/actions/marks/point/index.js +1 -0
- package/src/actions/marks/point/jitter.js +3 -0
- package/src/actions/marks/point/materialize.js +106 -2
- package/src/actions/marks/point/packing.js +68 -0
- package/src/actions/marks/references.js +87 -0
- package/src/actions/marks/remove.js +17 -4
- package/src/actions/marks/rule/actions.js +34 -3
- package/src/actions/marks/rule/appearance.js +26 -0
- package/src/actions/marks/shared.js +12 -12
- package/src/actions/marks/text/actions.js +224 -6
- package/src/actions/marks/text/layout.js +2 -2
- package/src/actions/primitives/createGraphics.js +3 -1
- package/src/actions/primitives/semantic.js +1 -1
- package/src/actions/primitives/semanticAction.js +39 -2
- package/src/actions/primitives/semanticValidation/guide.js +10 -0
- package/src/actions/primitives/semanticValidation/layer.js +18 -1
- package/src/actions/primitives/semanticValidation/scale.js +10 -1
- package/src/actions/regression/create.js +1 -1
- package/src/actions/regression/edit.js +19 -6
- package/src/actions/regression/resolve.js +1 -1
- package/src/actions/scales/consumers/common.js +20 -53
- package/src/actions/scales/consumers/families.js +27 -14
- package/src/actions/scales/consumers/index.js +23 -2
- package/src/actions/scales/consumers/seriesLayout.js +33 -2
- package/src/actions/scales/create.js +4 -4
- package/src/actions/scales/definitions.js +19 -3
- package/src/actions/scales/edit.js +46 -34
- package/src/actions/scales/editPolicy.js +21 -31
- package/src/actions/scales/materialize.js +10 -46
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +39 -0
- package/src/actions/scales/quantitativeColor.js +9 -5
- package/src/actions/theme/actions.js +43 -0
- package/src/actions/theme/index.js +9 -0
- package/src/actions/theme/reconcile.js +882 -0
- package/src/actions/titles/actions.js +11 -10
- package/src/actions/titles/resolve.js +1 -40
- package/src/actions/violinPlots/create.js +86 -42
- package/src/actions/violinPlots/edit.js +191 -0
- package/src/actions/violinPlots/index.js +2 -0
- package/src/core/ChartProgram.js +7 -0
- package/src/core/action.js +22 -2
- package/src/core/compositionState.js +136 -8
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +58 -1
- package/src/grammar/areaEndpoints.js +46 -0
- package/src/grammar/areaSeries.js +87 -41
- package/src/grammar/bars/aggregate.js +33 -12
- package/src/grammar/bars/policy.js +9 -7
- package/src/grammar/bin.js +181 -0
- package/src/grammar/categoryOrder.js +34 -0
- package/src/grammar/computed.js +150 -0
- package/src/grammar/ecdf.js +198 -0
- package/src/grammar/facets/index.js +117 -6
- package/src/grammar/fold.js +123 -0
- package/src/grammar/horizon.js +6 -4
- package/src/grammar/interval.js +37 -20
- package/src/grammar/lineSeries.js +20 -63
- package/src/grammar/markFilter.js +22 -4
- package/src/grammar/markLabels.js +93 -0
- package/src/grammar/parallelAxisTitles.js +16 -0
- package/src/grammar/pathSeries.js +91 -0
- package/src/grammar/pointPacking.js +183 -0
- package/src/grammar/positionDatum.js +12 -0
- package/src/grammar/rects.js +9 -1
- package/src/grammar/regression/derive.js +4 -0
- package/src/grammar/regression/index.js +7 -2
- package/src/grammar/regression/models.js +22 -7
- package/src/grammar/regression/parameters.js +40 -13
- package/src/grammar/rotation.js +29 -0
- package/src/grammar/rules.js +7 -21
- package/src/grammar/scales/color.js +23 -1
- package/src/grammar/scales/colorConsumers.js +38 -0
- package/src/grammar/scales/definition.js +15 -1
- package/src/grammar/scales/discretized.js +11 -5
- package/src/grammar/scales/fields.js +38 -8
- package/src/grammar/scales/mapping.js +2 -0
- package/src/grammar/scales/radial.js +55 -0
- package/src/grammar/scales/types.js +1 -0
- package/src/grammar/schemas/semanticPath.js +17 -3
- package/src/grammar/stack.js +170 -0
- package/src/grammar/statistics/confidenceInterval.js +101 -0
- package/src/grammar/summary.js +183 -0
- package/src/grammar/text.js +22 -16
- package/src/grammar/timeUnit.js +21 -11
- package/src/grammar/transformTopology.js +6 -0
- package/src/grammar/transforms.js +36 -0
- package/src/grammar/valueFormat.js +112 -0
- package/src/grammar/window.js +13 -7
- package/src/layout/facets.js +41 -7
- package/src/layout/guideCollisions.js +12 -0
- package/src/layout/legend.js +2 -10
- package/src/layout/legendItems.js +102 -0
- package/src/layout/legendLane.js +100 -11
- package/src/materialization/bars/aggregate.js +38 -152
- package/src/materialization/bars/histogram.js +24 -0
- package/src/materialization/categorySlotOffset.js +40 -0
- package/src/materialization/compositionSnapshot.js +1 -1
- package/src/materialization/dependencies.js +57 -16
- package/src/materialization/encodings.js +1 -1
- package/src/materialization/facets.js +8 -1
- package/src/materialization/guides/layout.js +57 -0
- package/src/materialization/guides/resources.js +2 -2
- package/src/materialization/legends.js +10 -1
- package/src/materialization/marks/capabilities.js +20 -5
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +58 -23
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +13 -13
- package/src/materialization/scaleGuideDependencies.js +8 -5
- package/src/materialization/scales/map.js +1 -0
- package/src/materialization/scales/policies/arc.js +42 -0
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +46 -14
- package/src/materialization/selection/compatibility.js +11 -0
- package/src/materialization/selection/items/bar.js +12 -68
- package/src/materialization/selection/items/common.js +8 -2
- package/src/materialization/selection/items/path.js +3 -13
- package/src/materialization/selection/items/rect.js +7 -4
- package/src/materialization/text.js +64 -5
- package/src/mcp/adapter.js +2 -2
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/layers.js +3 -2
- package/src/selectors/scales.js +6 -0
- package/src/theme/defaults.js +39 -0
- package/types/basic.d.ts +11 -2
- package/types/index.d.ts +126 -0
- package/types/program.d.ts +1320 -135
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -9,14 +9,20 @@ import {
|
|
|
9
9
|
validateGeneratedItemLimit
|
|
10
10
|
} from "../../../../core/validation.js";
|
|
11
11
|
import { formatTimeTick } from "../../../../grammar/ticks.js";
|
|
12
|
+
import {
|
|
13
|
+
formatValue,
|
|
14
|
+
validateValueFormat
|
|
15
|
+
} from "../../../../grammar/valueFormat.js";
|
|
12
16
|
import {
|
|
13
17
|
formatDistinctNumericSamples,
|
|
14
18
|
sampleNumericRange
|
|
15
19
|
} from "../../../../grammar/numeric.js";
|
|
16
20
|
import { resolveGraphicBounds } from "../../../../layout/canvas.js";
|
|
21
|
+
import { isHorizontalEdgeLegend } from "../../../../layout/legendLane.js";
|
|
17
22
|
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
18
23
|
"../../../../theme/defaults.js";
|
|
19
24
|
import { findLayer } from "../../../../selectors/layers.js";
|
|
25
|
+
import { isOpacityLegendLayer } from "../../../../materialization/legends.js";
|
|
20
26
|
import { findCanvasGraphic } from
|
|
21
27
|
"../../../../materialization/graphicHierarchy.js";
|
|
22
28
|
|
|
@@ -26,7 +32,7 @@ const OPTIONS = [
|
|
|
26
32
|
"direction", "columns", "titlePosition"
|
|
27
33
|
];
|
|
28
34
|
const TEXT_OPTIONS = [
|
|
29
|
-
"offset", "color", "fontSize", "fontFamily", "fontWeight"
|
|
35
|
+
"offset", "color", "fontSize", "fontFamily", "fontWeight", "format"
|
|
30
36
|
];
|
|
31
37
|
const BORDER_OPTIONS = [
|
|
32
38
|
"color", "lineWidth", "padding", "background"
|
|
@@ -82,9 +88,19 @@ export function normalizeLegendTextOptions(value, label, defaults) {
|
|
|
82
88
|
) {
|
|
83
89
|
throw new TypeError(`${label} fontWeight must be a string or finite number.`);
|
|
84
90
|
}
|
|
91
|
+
if (Object.hasOwn(result, "format")) {
|
|
92
|
+
result.format = validateValueFormat(result.format, `${label} format`);
|
|
93
|
+
}
|
|
85
94
|
return result;
|
|
86
95
|
}
|
|
87
96
|
|
|
97
|
+
export function normalizeLegendTitleOptions(value, label, defaults) {
|
|
98
|
+
if (value !== undefined) {
|
|
99
|
+
validateOptionObject(value, ["color", "fontSize", "fontFamily", "fontWeight"], label);
|
|
100
|
+
}
|
|
101
|
+
return normalizeLegendTextOptions(value, label, defaults);
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
export function normalizeLegendBorder(value) {
|
|
89
105
|
if (value === undefined || value === false) return false;
|
|
90
106
|
if (value !== true && !isPlainObject(value)) {
|
|
@@ -110,6 +126,9 @@ export function normalizeContinuousLegend(args, kind) {
|
|
|
110
126
|
if (!["left", "center", "right"].includes(align)) {
|
|
111
127
|
throw new Error(`Unsupported legend alignment "${align}".`);
|
|
112
128
|
}
|
|
129
|
+
if (["left", "right"].includes(position) && align !== "center") {
|
|
130
|
+
throw new Error("Side continuous legends require center alignment.");
|
|
131
|
+
}
|
|
113
132
|
const count = args.count ?? 5;
|
|
114
133
|
if (!Number.isInteger(count) || count < 2) {
|
|
115
134
|
throw new RangeError(
|
|
@@ -162,7 +181,7 @@ export function normalizeContinuousLegend(args, kind) {
|
|
|
162
181
|
? { ...DEFAULT_LABELS, offset: 8 }
|
|
163
182
|
: DEFAULT_LABELS
|
|
164
183
|
),
|
|
165
|
-
titleStyle:
|
|
184
|
+
titleStyle: normalizeLegendTitleOptions(
|
|
166
185
|
args.titleStyle,
|
|
167
186
|
"createLegend.titleStyle",
|
|
168
187
|
DEFAULT_TITLE
|
|
@@ -182,17 +201,17 @@ export function selectLegendLayer(program, requested, predicate) {
|
|
|
182
201
|
return candidates.includes(candidate) ? candidate : undefined;
|
|
183
202
|
}
|
|
184
203
|
|
|
185
|
-
export function
|
|
204
|
+
export function resolveContinuousLegendLayer(program, requested, channel) {
|
|
186
205
|
const layer = selectLegendLayer(
|
|
187
206
|
program,
|
|
188
207
|
requested,
|
|
189
|
-
|
|
190
|
-
candidate.encoding?.[channel]?.scale !== undefined
|
|
208
|
+
channel === "opacity" ? isOpacityLegendLayer : candidate =>
|
|
209
|
+
candidate.mark?.type === "point" && candidate.encoding?.[channel]?.scale !== undefined
|
|
191
210
|
);
|
|
192
211
|
if (layer === undefined) {
|
|
193
212
|
throw new Error(
|
|
194
213
|
requested === undefined
|
|
195
|
-
? `${channel} legend requires one eligible point mark.`
|
|
214
|
+
? `${channel} legend requires one eligible ${channel === "opacity" ? "point or line" : "point"} mark.`
|
|
196
215
|
: `Unknown ${channel} legend target "${requested}".`
|
|
197
216
|
);
|
|
198
217
|
}
|
|
@@ -251,10 +270,18 @@ export function sampleContinuousValues(domain, count) {
|
|
|
251
270
|
);
|
|
252
271
|
}
|
|
253
272
|
|
|
254
|
-
export function formatContinuousValues(values, domain, fieldType) {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
273
|
+
export function formatContinuousValues(values, domain, fieldType, format = "auto") {
|
|
274
|
+
const resolved = validateValueFormat(format, "Legend label format");
|
|
275
|
+
if (resolved === "auto") {
|
|
276
|
+
return fieldType === "temporal"
|
|
277
|
+
? values.map(value => formatTimeTick(value, domain))
|
|
278
|
+
: formatDistinctNumericSamples(values);
|
|
279
|
+
}
|
|
280
|
+
return values.map(value => formatValue(value, {
|
|
281
|
+
format: resolved,
|
|
282
|
+
valueType: fieldType === "temporal" ? "temporal" : "quantitative",
|
|
283
|
+
label: "Legend label format"
|
|
284
|
+
}));
|
|
258
285
|
}
|
|
259
286
|
|
|
260
287
|
export function styleContinuousText(
|
|
@@ -284,7 +311,10 @@ export function resolveLegendTextBounds(position, text, style) {
|
|
|
284
311
|
});
|
|
285
312
|
}
|
|
286
313
|
|
|
287
|
-
export function assertLegendBoundsInsideCanvas(bounds, canvas, label) {
|
|
314
|
+
export function assertLegendBoundsInsideCanvas(bounds, canvas, label, config) {
|
|
315
|
+
// Horizontal content is intrinsic until the shared lane places its actual
|
|
316
|
+
// concrete bounds. Only that final placement can determine Canvas fit.
|
|
317
|
+
if (isHorizontalEdgeLegend(config)) return;
|
|
288
318
|
if (bounds.some(item =>
|
|
289
319
|
item.left < 0 || item.right > canvas.width ||
|
|
290
320
|
item.top < 0 || item.bottom > canvas.height
|
|
@@ -297,7 +327,8 @@ export function resolveLegendBackgroundFromBounds(
|
|
|
297
327
|
bounds,
|
|
298
328
|
border,
|
|
299
329
|
canvas,
|
|
300
|
-
label
|
|
330
|
+
label,
|
|
331
|
+
config
|
|
301
332
|
) {
|
|
302
333
|
if (border === false) return undefined;
|
|
303
334
|
const strokeExtent = border.lineWidth / 2;
|
|
@@ -315,11 +346,11 @@ export function resolveLegendBackgroundFromBounds(
|
|
|
315
346
|
y -= border.padding;
|
|
316
347
|
right += border.padding;
|
|
317
348
|
bottom += border.padding;
|
|
318
|
-
if (
|
|
349
|
+
if (!isHorizontalEdgeLegend(config) && (
|
|
319
350
|
x - strokeExtent < 0 || y - strokeExtent < 0 ||
|
|
320
351
|
right + strokeExtent > canvas.width ||
|
|
321
352
|
bottom + strokeExtent > canvas.height
|
|
322
|
-
) {
|
|
353
|
+
)) {
|
|
323
354
|
throw new Error(`${label} background requires more Canvas margin space.`);
|
|
324
355
|
}
|
|
325
356
|
return { x, y, width: right - x, height: bottom - y };
|
|
@@ -334,3 +365,27 @@ export function editLegendBackground(program, id, bounds, border) {
|
|
|
334
365
|
strokeWidth: border.lineWidth
|
|
335
366
|
});
|
|
336
367
|
}
|
|
368
|
+
|
|
369
|
+
export function normalizeItemLegendLayout(args) {
|
|
370
|
+
const position = args.position ?? "right";
|
|
371
|
+
if (!["right", "left", "top", "bottom"].includes(position)) throw new Error(`Unsupported legend position "${position}".`);
|
|
372
|
+
const side = ["left", "right"].includes(position);
|
|
373
|
+
const layout = args.layout === undefined ? "edge" : args.layout;
|
|
374
|
+
if (layout !== "edge") throw new Error('This legend requires layout "edge".');
|
|
375
|
+
const align = args.align ?? "center";
|
|
376
|
+
if (!["left", "center", "right"].includes(align)) throw new Error(`Unsupported legend alignment "${align}".`);
|
|
377
|
+
const direction = args.direction ?? (side ? "vertical" : "horizontal");
|
|
378
|
+
if (!["horizontal", "vertical"].includes(direction)) throw new Error(`Unsupported legend direction "${direction}".`);
|
|
379
|
+
const columns = args.columns;
|
|
380
|
+
if (columns !== undefined && (!Number.isInteger(columns) || columns < 1)) throw new RangeError("Legend columns must be a positive integer.");
|
|
381
|
+
const titlePosition = args.titlePosition ?? "top";
|
|
382
|
+
if (!["top", "left"].includes(titlePosition)) throw new Error(`Unsupported legend titlePosition "${titlePosition}".`);
|
|
383
|
+
if (side && (direction !== "vertical" || align !== "center" || (columns !== undefined && columns !== 1) || titlePosition !== "top")) {
|
|
384
|
+
throw new Error("Side item legends require vertical direction, center alignment, top title and one column.");
|
|
385
|
+
}
|
|
386
|
+
const offset = args.offset ?? 30;
|
|
387
|
+
const itemGap = args.itemGap ?? 28;
|
|
388
|
+
validateNonNegative(offset, "Legend offset");
|
|
389
|
+
validatePositive(itemGap, "Legend itemGap");
|
|
390
|
+
return { position, layout, align, direction, columns, titlePosition, offset, itemGap };
|
|
391
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { action } from "../../../../core/action.js";
|
|
2
2
|
import { isPlainObject } from "../../../../core/immutable.js";
|
|
3
3
|
import { validateKeys } from "../../../../core/validation.js";
|
|
4
|
-
import {
|
|
4
|
+
import { mapScaleConsumerValues } from "../../../../materialization/scales/map.js";
|
|
5
|
+
import { inverseLerp, interpolateNumber } from "../../../../grammar/numeric.js";
|
|
5
6
|
import { DEFAULT_COLORS } from "../../../../theme/defaults.js";
|
|
6
7
|
import {
|
|
7
8
|
assertLegendBoundsInsideCanvas,
|
|
@@ -20,6 +21,8 @@ import {
|
|
|
20
21
|
import { resolveLegendGraphicPlacement } from
|
|
21
22
|
"../../../../materialization/graphicHierarchy.js";
|
|
22
23
|
|
|
24
|
+
export const DEFAULT_GRADIENT_SIZE = Object.freeze({ length: 120, thickness: 12 });
|
|
25
|
+
|
|
23
26
|
const GRADIENT_OPTIONS = Object.freeze(["length", "thickness"]);
|
|
24
27
|
|
|
25
28
|
function resolveGradientLayout(program, config, scale) {
|
|
@@ -43,7 +46,7 @@ function resolveGradientLayout(program, config, scale) {
|
|
|
43
46
|
? plot.y - config.offset - thickness - config.labels.offset -
|
|
44
47
|
config.labels.fontSize
|
|
45
48
|
: plot.y + plot.height + config.offset +
|
|
46
|
-
config.titleStyle.fontSize + 12;
|
|
49
|
+
(config.titleVisible === false ? 0 : config.titleStyle.fontSize + 12);
|
|
47
50
|
}
|
|
48
51
|
const title = vertical
|
|
49
52
|
? { x, y: plot.y + 20, align: "left" }
|
|
@@ -52,9 +55,20 @@ function resolveGradientLayout(program, config, scale) {
|
|
|
52
55
|
y: y - 12 - config.titleStyle.fontSize / 2,
|
|
53
56
|
align: "center"
|
|
54
57
|
};
|
|
55
|
-
const values = sampleContinuousValues(scale.domain, config.count);
|
|
56
|
-
|
|
57
|
-
|
|
58
|
+
const values = [...sampleContinuousValues(scale.domain, config.count)];
|
|
59
|
+
if (scale.midpoint !== undefined && !values.includes(scale.midpoint)) {
|
|
60
|
+
values.push(scale.midpoint);
|
|
61
|
+
values.sort((a, b) => scale.domain[1] > scale.domain[0] ? a - b : b - a);
|
|
62
|
+
}
|
|
63
|
+
const texts = formatContinuousValues(
|
|
64
|
+
values,
|
|
65
|
+
scale.domain,
|
|
66
|
+
config.fieldType,
|
|
67
|
+
config.labels.format
|
|
68
|
+
);
|
|
69
|
+
const fractions = values.map((value, index) => scale.midpoint === undefined
|
|
70
|
+
? index / (values.length - 1)
|
|
71
|
+
: inverseLerp(value, ...scale.domain));
|
|
58
72
|
const labelOffset = config.labels.offset;
|
|
59
73
|
const labels = vertical
|
|
60
74
|
? fractions.map(fraction => ({
|
|
@@ -97,22 +111,23 @@ function resolveGradientLayout(program, config, scale) {
|
|
|
97
111
|
const labelBounds = labels.map((label, index) =>
|
|
98
112
|
resolveLegendTextBounds(label, texts[index], config.labels)
|
|
99
113
|
);
|
|
100
|
-
const titleBounds = resolveLegendTextBounds(
|
|
114
|
+
const titleBounds = config.titleVisible === false ? undefined : resolveLegendTextBounds(
|
|
101
115
|
title,
|
|
102
116
|
config.title,
|
|
103
117
|
config.titleStyle
|
|
104
118
|
);
|
|
105
|
-
const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
|
|
119
|
+
const occupiedBounds = [stripBounds, ...tickBounds, ...labelBounds,
|
|
120
|
+
...(config.titleVisible === false ? [] : [titleBounds])];
|
|
106
121
|
assertLegendBoundsInsideCanvas(
|
|
107
122
|
occupiedBounds,
|
|
108
123
|
canvas,
|
|
109
|
-
"Gradient legend layout"
|
|
124
|
+
"Gradient legend layout", config
|
|
110
125
|
);
|
|
111
126
|
const background = resolveLegendBackgroundFromBounds(
|
|
112
127
|
occupiedBounds,
|
|
113
128
|
config.border,
|
|
114
129
|
canvas,
|
|
115
|
-
"Gradient legend"
|
|
130
|
+
"Gradient legend", config
|
|
116
131
|
);
|
|
117
132
|
return {
|
|
118
133
|
vertical, x, y, length, thickness, values, texts, labels, ticks, title,
|
|
@@ -163,11 +178,11 @@ export const rematerializeGradientLegend = action(
|
|
|
163
178
|
const stripSize = layout.length / stripCount;
|
|
164
179
|
const strips = Array.from({ length: stripCount }, (_, index) => {
|
|
165
180
|
const fraction = (index + 0.5) / stripCount;
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
);
|
|
181
|
+
const position = layout.vertical ? 1 - fraction : fraction;
|
|
182
|
+
// Keep legacy uniform samples exact without a value/domain round trip.
|
|
183
|
+
const samplingScale = scale.midpoint === undefined ? { ...scale, domain: [0, 1] } : scale;
|
|
184
|
+
const value = scale.midpoint === undefined ? position : interpolateNumber(...scale.domain, position);
|
|
185
|
+
const [color] = mapScaleConsumerValues([value], samplingScale, "color");
|
|
171
186
|
return {
|
|
172
187
|
x: layout.x + (layout.vertical ? 0 : index * stripSize),
|
|
173
188
|
y: layout.y + (layout.vertical ? index * stripSize : 0),
|
|
@@ -283,86 +298,99 @@ export const rematerializeGradientLegend = action(
|
|
|
283
298
|
}
|
|
284
299
|
);
|
|
285
300
|
|
|
301
|
+
|
|
302
|
+
export function resolveGradientLegendCreation(program, args = {}) {
|
|
303
|
+
const config = normalizeContinuousLegend(args, "gradient");
|
|
304
|
+
if (args.channels !== undefined && (
|
|
305
|
+
!Array.isArray(args.channels) ||
|
|
306
|
+
args.channels.length !== 1 ||
|
|
307
|
+
args.channels[0] !== "color"
|
|
308
|
+
)) {
|
|
309
|
+
throw new Error('Gradient legend requires channels: ["color"].');
|
|
310
|
+
}
|
|
311
|
+
if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
|
|
312
|
+
throw new TypeError("createLegend.gradient must be a plain object.");
|
|
313
|
+
}
|
|
314
|
+
validateKeys(
|
|
315
|
+
args.gradient ?? {},
|
|
316
|
+
GRADIENT_OPTIONS,
|
|
317
|
+
"createLegend.gradient"
|
|
318
|
+
);
|
|
319
|
+
config.gradient = {
|
|
320
|
+
length: args.gradient?.length ?? DEFAULT_GRADIENT_SIZE.length,
|
|
321
|
+
thickness: args.gradient?.thickness ?? DEFAULT_GRADIENT_SIZE.thickness
|
|
322
|
+
};
|
|
323
|
+
config.titleVisible = true;
|
|
324
|
+
validatePositive(config.gradient.length, "Gradient length");
|
|
325
|
+
validatePositive(config.gradient.thickness, "Gradient thickness");
|
|
326
|
+
const resolved = resolveGradientConfig(program, config);
|
|
327
|
+
return resolved;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function createGradientLegendFromConfig(program, config) {
|
|
331
|
+
const resolved = resolveGradientConfig(program, config);
|
|
332
|
+
resolveGradientLayout(program, resolved.config, resolved.scale);
|
|
333
|
+
let next = program
|
|
334
|
+
.editSemantic({
|
|
335
|
+
property: "guide.legend.color.scale",
|
|
336
|
+
value: resolved.encoding.scale
|
|
337
|
+
})
|
|
338
|
+
.editSemantic({
|
|
339
|
+
property: "guide.legend.color.title",
|
|
340
|
+
value: resolved.config.title
|
|
341
|
+
})
|
|
342
|
+
._withLegendConfig("gradient", resolved.config);
|
|
343
|
+
if (resolved.config.border !== false) {
|
|
344
|
+
next = next.createGraphics({
|
|
345
|
+
id: "colorGradientBackground",
|
|
346
|
+
type: "rect",
|
|
347
|
+
...resolveLegendGraphicPlacement(next)
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
next = next
|
|
351
|
+
.createGraphics({
|
|
352
|
+
id: "colorGradientStrips",
|
|
353
|
+
type: "rect",
|
|
354
|
+
length: 0,
|
|
355
|
+
...resolveLegendGraphicPlacement(next, resolved.config.border === false
|
|
356
|
+
? {}
|
|
357
|
+
: { after: "colorGradientBackground" })
|
|
358
|
+
})
|
|
359
|
+
.createGraphics({
|
|
360
|
+
id: "colorGradientTicks",
|
|
361
|
+
type: "line",
|
|
362
|
+
length: 0,
|
|
363
|
+
...resolveLegendGraphicPlacement(next)
|
|
364
|
+
})
|
|
365
|
+
.createGraphics({
|
|
366
|
+
id: "colorGradientLabels",
|
|
367
|
+
type: "text",
|
|
368
|
+
length: 0,
|
|
369
|
+
...resolveLegendGraphicPlacement(next)
|
|
370
|
+
});
|
|
371
|
+
if (resolved.config.titleVisible !== false) {
|
|
372
|
+
next = next.createGraphics({
|
|
373
|
+
id: "colorGradientTitle",
|
|
374
|
+
type: "text",
|
|
375
|
+
...resolveLegendGraphicPlacement(next)
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
return next.rematerializeGradientLegend();
|
|
379
|
+
}
|
|
380
|
+
|
|
286
381
|
export const createGradientLegend = action(
|
|
287
382
|
{
|
|
288
383
|
op: "createGradientLegend",
|
|
289
384
|
description: "Create a continuous color gradient legend."
|
|
290
385
|
},
|
|
291
386
|
function (args = {}) {
|
|
292
|
-
const
|
|
293
|
-
if (args.channels !== undefined && (
|
|
294
|
-
!Array.isArray(args.channels) ||
|
|
295
|
-
args.channels.length !== 1 ||
|
|
296
|
-
args.channels[0] !== "color"
|
|
297
|
-
)) {
|
|
298
|
-
throw new Error('Gradient legend requires channels: ["color"].');
|
|
299
|
-
}
|
|
300
|
-
if (args.gradient !== undefined && !isPlainObject(args.gradient)) {
|
|
301
|
-
throw new TypeError("createLegend.gradient must be a plain object.");
|
|
302
|
-
}
|
|
303
|
-
validateKeys(
|
|
304
|
-
args.gradient ?? {},
|
|
305
|
-
GRADIENT_OPTIONS,
|
|
306
|
-
"createLegend.gradient"
|
|
307
|
-
);
|
|
308
|
-
config.gradient = {
|
|
309
|
-
length: args.gradient?.length ?? 120,
|
|
310
|
-
thickness: args.gradient?.thickness ?? 12
|
|
311
|
-
};
|
|
312
|
-
config.titleVisible = true;
|
|
313
|
-
validatePositive(config.gradient.length, "Gradient length");
|
|
314
|
-
validatePositive(config.gradient.thickness, "Gradient thickness");
|
|
315
|
-
const resolved = resolveGradientConfig(this, config);
|
|
387
|
+
const resolved = resolveGradientLegendCreation(this, args);
|
|
316
388
|
resolveGradientLayout(this, resolved.config, resolved.scale);
|
|
317
389
|
if (this.graphicSpec.objects.colorGradientStrips !== undefined) {
|
|
318
390
|
throw new Error(
|
|
319
391
|
"createGradientLegend requires a missing gradient legend."
|
|
320
392
|
);
|
|
321
393
|
}
|
|
322
|
-
|
|
323
|
-
.editSemantic({
|
|
324
|
-
property: "guide.legend.color.scale",
|
|
325
|
-
value: resolved.encoding.scale
|
|
326
|
-
})
|
|
327
|
-
.editSemantic({
|
|
328
|
-
property: "guide.legend.color.title",
|
|
329
|
-
value: resolved.config.title
|
|
330
|
-
})
|
|
331
|
-
._withLegendConfig("gradient", resolved.config);
|
|
332
|
-
if (resolved.config.border !== false) {
|
|
333
|
-
next = next.createGraphics({
|
|
334
|
-
id: "colorGradientBackground",
|
|
335
|
-
type: "rect",
|
|
336
|
-
...resolveLegendGraphicPlacement(next)
|
|
337
|
-
});
|
|
338
|
-
}
|
|
339
|
-
return next
|
|
340
|
-
.createGraphics({
|
|
341
|
-
id: "colorGradientStrips",
|
|
342
|
-
type: "rect",
|
|
343
|
-
length: 0,
|
|
344
|
-
...resolveLegendGraphicPlacement(next),
|
|
345
|
-
...(resolved.config.border === false
|
|
346
|
-
? {}
|
|
347
|
-
: { after: "colorGradientBackground" })
|
|
348
|
-
})
|
|
349
|
-
.createGraphics({
|
|
350
|
-
id: "colorGradientTicks",
|
|
351
|
-
type: "line",
|
|
352
|
-
length: 0,
|
|
353
|
-
...resolveLegendGraphicPlacement(next)
|
|
354
|
-
})
|
|
355
|
-
.createGraphics({
|
|
356
|
-
id: "colorGradientLabels",
|
|
357
|
-
type: "text",
|
|
358
|
-
length: 0,
|
|
359
|
-
...resolveLegendGraphicPlacement(next)
|
|
360
|
-
})
|
|
361
|
-
.createGraphics({
|
|
362
|
-
id: "colorGradientTitle",
|
|
363
|
-
type: "text",
|
|
364
|
-
...resolveLegendGraphicPlacement(next)
|
|
365
|
-
})
|
|
366
|
-
.rematerializeGradientLegend();
|
|
394
|
+
return createGradientLegendFromConfig(this, resolved.config);
|
|
367
395
|
}
|
|
368
396
|
);
|
|
@@ -19,11 +19,15 @@ export function registerContinuousLegendActions(ProgramClass) {
|
|
|
19
19
|
ProgramClass.prototype.createOpacityLegend = createOpacityLegend;
|
|
20
20
|
ProgramClass.prototype.rematerializeOpacityLegend = rematerializeOpacityLegend;
|
|
21
21
|
ProgramClass.prototype.removeOpacityLegend = removeOpacityLegend;
|
|
22
|
-
ProgramClass
|
|
23
|
-
ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
|
|
22
|
+
registerIntervalLegendActions(ProgramClass);
|
|
24
23
|
}
|
|
25
24
|
|
|
26
25
|
export function registerGradientLegendActions(ProgramClass) {
|
|
27
26
|
ProgramClass.prototype.createGradientLegend = createGradientLegend;
|
|
28
27
|
ProgramClass.prototype.rematerializeGradientLegend = rematerializeGradientLegend;
|
|
29
28
|
}
|
|
29
|
+
|
|
30
|
+
export function registerIntervalLegendActions(ProgramClass) {
|
|
31
|
+
ProgramClass.prototype.createIntervalLegend = createIntervalLegend;
|
|
32
|
+
ProgramClass.prototype.rematerializeIntervalLegend = rematerializeIntervalLegend;
|
|
33
|
+
}
|