ggaction 0.0.11 → 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 +80 -0
- package/README.md +3 -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 +33 -27
- package/src/actions/encodings/parallel.js +2 -8
- package/src/actions/encodings/position/apply.js +40 -4
- package/src/actions/encodings/position/index.js +43 -5
- package/src/actions/encodings/position/policies/arc.js +37 -2
- 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 +79 -24
- 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 +153 -11
- package/src/actions/errorBars/edit.js +365 -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 +138 -14
- 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 +60 -4
- 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/core/vocabulary.js +2 -2
- package/src/grammar/aggregate.js +66 -23
- package/src/grammar/arcs.js +213 -65
- 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/positionCompatibility.js +1 -1
- 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 +9 -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/ordinal.js +23 -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 +86 -23
- 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/facetGuides/legacyCategorical.js +7 -1
- package/src/materialization/facetGuides/placement.js +16 -1
- package/src/materialization/facetGuides/preparation.js +26 -1
- package/src/materialization/facets.js +11 -2
- 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 +30 -7
- package/src/materialization/marks/grouping.js +52 -0
- package/src/materialization/marks/index.js +63 -25
- package/src/materialization/marks/policies.js +2 -0
- package/src/materialization/rect.js +33 -33
- package/src/materialization/rowEncoding.js +70 -14
- 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/bar.js +0 -43
- package/src/materialization/scales/policies/binnedPosition.js +7 -4
- package/src/materialization/scales/policies/offset.js +54 -0
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +48 -18
- 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 +1327 -124
- package/src/materialization/bars/grouped.js +0 -144
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
normalizePointJitterPolicy,
|
|
12
12
|
resolvePointJitter
|
|
13
13
|
} from "../../../grammar/jitter.js";
|
|
14
|
+
import {
|
|
15
|
+
normalizePointPackingPolicy,
|
|
16
|
+
resolvePointPacking
|
|
17
|
+
} from "../../../grammar/pointPacking.js";
|
|
14
18
|
import { polarToCartesian, resolvePolarFrame } from "../../../grammar/polar.js";
|
|
15
19
|
import { resolveDirectionValues } from "../../../grammar/direction.js";
|
|
16
20
|
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
@@ -21,9 +25,15 @@ import {
|
|
|
21
25
|
} from "../../../theme/defaults.js";
|
|
22
26
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
23
27
|
import { findLayer } from "../../../selectors/layers.js";
|
|
28
|
+
import { canMaterializePoint } from "../../../materialization/marks/capabilities.js";
|
|
24
29
|
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
25
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
resolveRowEncodingValues,
|
|
32
|
+
resolveRowPositionValues
|
|
33
|
+
} from
|
|
26
34
|
"../../../materialization/rowEncoding.js";
|
|
35
|
+
import { offsetCategoryPositions } from
|
|
36
|
+
"../../../materialization/categorySlotOffset.js";
|
|
27
37
|
|
|
28
38
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
29
39
|
const DEFAULT_POINT_FILL = DEFAULT_COLORS.mark;
|
|
@@ -33,8 +43,8 @@ function resolvePointPositions(program, layer, dataset) {
|
|
|
33
43
|
layer.encoding?.radius !== undefined;
|
|
34
44
|
if (!hasPolar) {
|
|
35
45
|
return {
|
|
36
|
-
x:
|
|
37
|
-
y:
|
|
46
|
+
x: resolveRowPositionValues(program, layer, dataset, "x"),
|
|
47
|
+
y: resolveRowPositionValues(program, layer, dataset, "y")
|
|
38
48
|
};
|
|
39
49
|
}
|
|
40
50
|
const theta = resolveRowEncodingValues(program, layer, dataset, "theta");
|
|
@@ -207,6 +217,80 @@ function applyPointJitter(program, {
|
|
|
207
217
|
};
|
|
208
218
|
}
|
|
209
219
|
|
|
220
|
+
function applyPointPacking(program, {
|
|
221
|
+
id,
|
|
222
|
+
layer,
|
|
223
|
+
dataset,
|
|
224
|
+
x,
|
|
225
|
+
y,
|
|
226
|
+
area,
|
|
227
|
+
shapes,
|
|
228
|
+
angles,
|
|
229
|
+
config,
|
|
230
|
+
graphic,
|
|
231
|
+
existingChildren
|
|
232
|
+
}) {
|
|
233
|
+
const stored = program.materializationConfigs.pointPacking?.[id];
|
|
234
|
+
if (stored === undefined) return { program, x, y };
|
|
235
|
+
const policy = normalizePointPackingPolicy(stored);
|
|
236
|
+
const encoding = layer.encoding?.[policy.channel];
|
|
237
|
+
const scale = program.resolvedScales[encoding?.scale];
|
|
238
|
+
if (scale === undefined) {
|
|
239
|
+
throw new Error(`Point packing on "${id}" requires a resolved ${policy.channel} scale.`);
|
|
240
|
+
}
|
|
241
|
+
const seen = new Set();
|
|
242
|
+
const entries = dataset.values.flatMap((row, index) => {
|
|
243
|
+
if (!Number.isFinite(x?.[index]) || !Number.isFinite(y?.[index])) return [];
|
|
244
|
+
const rawIdentity = policy.key === undefined ? index : row[policy.key];
|
|
245
|
+
const identity = canonicalJitterScalar(
|
|
246
|
+
rawIdentity,
|
|
247
|
+
policy.key === undefined
|
|
248
|
+
? "Point packing source index"
|
|
249
|
+
: `Point packing key field "${policy.key}"`
|
|
250
|
+
);
|
|
251
|
+
if (seen.has(identity)) {
|
|
252
|
+
throw new Error(`Point packing key field "${policy.key}" must be unique for materialized items.`);
|
|
253
|
+
}
|
|
254
|
+
seen.add(identity);
|
|
255
|
+
const resolvedArea = resolvedPointArea(
|
|
256
|
+
area, index, config, existingChildren, graphic.type
|
|
257
|
+
);
|
|
258
|
+
const extent = resolvePointShapeExtent({
|
|
259
|
+
shape: shapes[index],
|
|
260
|
+
area: resolvedArea,
|
|
261
|
+
strokeWidth: typeof config.stroke === "string" ? config.strokeWidth ?? 1 : 0,
|
|
262
|
+
angle: angles?.[index]
|
|
263
|
+
});
|
|
264
|
+
return [{
|
|
265
|
+
index,
|
|
266
|
+
identity,
|
|
267
|
+
base: policy.channel === "x" ? x[index] : y[index],
|
|
268
|
+
fixed: policy.channel === "x" ? y[index] : x[index],
|
|
269
|
+
halfExtent: extent[policy.channel],
|
|
270
|
+
fixedHalfExtent: extent[policy.channel === "x" ? "y" : "x"]
|
|
271
|
+
}];
|
|
272
|
+
});
|
|
273
|
+
const bounds = resolveGraphicBounds(program);
|
|
274
|
+
const resolution = resolvePointPacking({
|
|
275
|
+
target: id,
|
|
276
|
+
policy,
|
|
277
|
+
scale,
|
|
278
|
+
entries,
|
|
279
|
+
plotMinimum: policy.channel === "x" ? bounds.x : bounds.y,
|
|
280
|
+
plotMaximum: policy.channel === "x" ? bounds.x + bounds.width : bounds.y + bounds.height
|
|
281
|
+
});
|
|
282
|
+
const values = [...(policy.channel === "x" ? x : y)];
|
|
283
|
+
for (const item of resolution.items) values[item.index] = item.final;
|
|
284
|
+
return {
|
|
285
|
+
program: program._withMaterializationConfig(["pointPacking", id], {
|
|
286
|
+
...policy,
|
|
287
|
+
resolved: resolution
|
|
288
|
+
}),
|
|
289
|
+
x: policy.channel === "x" ? values : x,
|
|
290
|
+
y: policy.channel === "y" ? values : y
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
210
294
|
export const rematerializePointMark = action(
|
|
211
295
|
{
|
|
212
296
|
op: "rematerializePointMark",
|
|
@@ -222,8 +306,20 @@ export const rematerializePointMark = action(
|
|
|
222
306
|
resetValue: []
|
|
223
307
|
});
|
|
224
308
|
if (highlighted !== undefined) return highlighted;
|
|
225
|
-
|
|
226
|
-
const
|
|
309
|
+
let resolved = this;
|
|
310
|
+
const initialLayer = findLayer(resolved, id);
|
|
311
|
+
for (const channel of ["xOffset", "yOffset"]) {
|
|
312
|
+
const scaleId = initialLayer?.encoding?.[channel]?.scale;
|
|
313
|
+
if (scaleId !== undefined) {
|
|
314
|
+
resolved = resolved.rematerializeScale({
|
|
315
|
+
id: scaleId,
|
|
316
|
+
guides: false,
|
|
317
|
+
marks: false
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
const layer = findLayer(resolved, id);
|
|
322
|
+
const graphic = resolved.graphicSpec.objects[id];
|
|
227
323
|
|
|
228
324
|
if (layer?.mark?.type !== "point") {
|
|
229
325
|
throw new Error(`Unknown point mark "${id}".`);
|
|
@@ -231,18 +327,27 @@ export const rematerializePointMark = action(
|
|
|
231
327
|
if (!["circle", "rect", "path", "collection"].includes(graphic?.type)) {
|
|
232
328
|
throw new Error(`Point mark "${id}" requires point graphics.`);
|
|
233
329
|
}
|
|
234
|
-
const dataset = findDataset(
|
|
330
|
+
const dataset = findDataset(resolved, layer.data);
|
|
235
331
|
if (dataset === undefined) {
|
|
236
332
|
throw new Error(`Point mark "${id}" requires an existing dataset.`);
|
|
237
333
|
}
|
|
238
334
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const
|
|
335
|
+
if (graphic.items.length === 0 && !canMaterializePoint(resolved, layer)) {
|
|
336
|
+
return resolved;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const positions = resolvePointPositions(resolved, layer, dataset);
|
|
340
|
+
const mappedFill = resolveRowEncodingValues(resolved, layer, dataset, "color");
|
|
341
|
+
const area = resolveRowEncodingValues(resolved, layer, dataset, "size");
|
|
342
|
+
const encodedShape = resolveRowEncodingValues(resolved, layer, dataset, "shape");
|
|
343
|
+
const encodedOpacity = resolveRowEncodingValues(
|
|
344
|
+
resolved,
|
|
345
|
+
layer,
|
|
346
|
+
dataset,
|
|
347
|
+
"opacity"
|
|
348
|
+
);
|
|
244
349
|
const angles = resolveDirectionValues(dataset.values, layer.encoding?.angle);
|
|
245
|
-
const config =
|
|
350
|
+
const config = resolved.markConfigs[id] ?? {};
|
|
246
351
|
const fill = mappedFill ?? config.fill ?? DEFAULT_POINT_FILL;
|
|
247
352
|
const shapes = encodedShape ?? dataset.values.map(() => config.shape ?? "circle");
|
|
248
353
|
const existingChildren = graphic.items ?? [];
|
|
@@ -252,7 +357,13 @@ export const rematerializePointMark = action(
|
|
|
252
357
|
angles !== undefined ||
|
|
253
358
|
graphic.type === "collection" ||
|
|
254
359
|
getPointGraphicType(constantShape) !== graphic.type;
|
|
255
|
-
|
|
360
|
+
if (
|
|
361
|
+
resolved.materializationConfigs.jitters?.[id] !== undefined &&
|
|
362
|
+
resolved.materializationConfigs.pointPacking?.[id] !== undefined
|
|
363
|
+
) {
|
|
364
|
+
throw new Error(`Point mark "${id}" cannot combine jitter and packing.`);
|
|
365
|
+
}
|
|
366
|
+
const packed = applyPointPacking(resolved, {
|
|
256
367
|
id,
|
|
257
368
|
layer,
|
|
258
369
|
dataset,
|
|
@@ -265,7 +376,20 @@ export const rematerializePointMark = action(
|
|
|
265
376
|
graphic,
|
|
266
377
|
existingChildren
|
|
267
378
|
});
|
|
268
|
-
const
|
|
379
|
+
const jittered = applyPointJitter(packed.program, {
|
|
380
|
+
id,
|
|
381
|
+
layer,
|
|
382
|
+
dataset,
|
|
383
|
+
x: packed.x,
|
|
384
|
+
y: packed.y,
|
|
385
|
+
area,
|
|
386
|
+
shapes,
|
|
387
|
+
angles,
|
|
388
|
+
config,
|
|
389
|
+
graphic,
|
|
390
|
+
existingChildren
|
|
391
|
+
});
|
|
392
|
+
const { x, y } = offsetCategoryPositions(jittered.program, layer, jittered);
|
|
269
393
|
|
|
270
394
|
if (requiresMixedCollection) {
|
|
271
395
|
const items = dataset.values.map((_, index) => {
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { action } from "../../../core/action.js";
|
|
2
|
+
import { validateUserId } from "../../../core/identifiers.js";
|
|
3
|
+
import { validateKeys } from "../../../core/validation.js";
|
|
4
|
+
import { normalizePointPackingPolicy } from "../../../grammar/pointPacking.js";
|
|
5
|
+
import { findCoordinate } from "../../../selectors/coordinates.js";
|
|
6
|
+
import { resolveEligibleLayer } from "../../../selectors/layers.js";
|
|
7
|
+
|
|
8
|
+
const OPTIONS = Object.freeze([
|
|
9
|
+
"target", "channel", "maxOffset", "padding", "key", "overflow"
|
|
10
|
+
]);
|
|
11
|
+
const REMOVE_OPTIONS = Object.freeze(["target"]);
|
|
12
|
+
|
|
13
|
+
function isPackablePoint(program, layer, channel) {
|
|
14
|
+
const packed = layer.encoding?.[channel];
|
|
15
|
+
const fixed = layer.encoding?.[channel === "x" ? "y" : "x"];
|
|
16
|
+
return layer.mark?.type === "point" &&
|
|
17
|
+
["nominal", "ordinal"].includes(packed?.fieldType) &&
|
|
18
|
+
["quantitative", "temporal"].includes(fixed?.fieldType) &&
|
|
19
|
+
packed.scale !== undefined && fixed.scale !== undefined &&
|
|
20
|
+
findCoordinate(program, layer.coordinate)?.type === "cartesian";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function target(program, requested, channel) {
|
|
24
|
+
return resolveEligibleLayer(program, {
|
|
25
|
+
target: requested === undefined ? undefined : validateUserId(requested, "Point packing target"),
|
|
26
|
+
predicate: layer => isPackablePoint(program, layer, channel),
|
|
27
|
+
label: "point packing"
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const packPoints = action(
|
|
32
|
+
{ op: "packPoints", description: "Pack point glyphs deterministically within categorical slots." },
|
|
33
|
+
function (args = {}) {
|
|
34
|
+
validateKeys(args, OPTIONS, "packPoints");
|
|
35
|
+
const policy = normalizePointPackingPolicy(args);
|
|
36
|
+
const layer = target(this, args.target, policy.channel);
|
|
37
|
+
if (this.materializationConfigs.jitters?.[layer.id] !== undefined) {
|
|
38
|
+
throw new Error(`Point packing on "${layer.id}" conflicts with point jitter.`);
|
|
39
|
+
}
|
|
40
|
+
return this
|
|
41
|
+
._withMaterializationConfig(["pointPacking", layer.id], policy)
|
|
42
|
+
.rematerializePointMark({ id: layer.id });
|
|
43
|
+
}
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
export const removePointPacking = action(
|
|
47
|
+
{ op: "removePointPacking", description: "Remove point packing and restore semantic positions." },
|
|
48
|
+
function (args = {}) {
|
|
49
|
+
validateKeys(args, REMOVE_OPTIONS, "removePointPacking");
|
|
50
|
+
const requested = args.target === undefined
|
|
51
|
+
? undefined
|
|
52
|
+
: validateUserId(args.target, "Point packing target");
|
|
53
|
+
const layer = resolveEligibleLayer(this, {
|
|
54
|
+
target: requested,
|
|
55
|
+
predicate: candidate => candidate.mark?.type === "point" &&
|
|
56
|
+
this.materializationConfigs.pointPacking?.[candidate.id] !== undefined,
|
|
57
|
+
label: "point packing"
|
|
58
|
+
});
|
|
59
|
+
return this
|
|
60
|
+
._withoutMaterializationConfig(["pointPacking", layer.id])
|
|
61
|
+
.rematerializePointMark({ id: layer.id });
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
export function registerPointPackingActions(ProgramClass) {
|
|
66
|
+
ProgramClass.prototype.packPoints = packPoints;
|
|
67
|
+
ProgramClass.prototype.removePointPacking = removePointPacking;
|
|
68
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { isSourceOwnedText } from "../../grammar/text.js";
|
|
2
|
+
import { action } from "../../core/action.js";
|
|
3
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
4
|
+
import { findCoordinate } from "../../selectors/coordinates.js";
|
|
5
|
+
import { resolveEligibleLayer } from "../../selectors/layers.js";
|
|
6
|
+
import { resolveFacadeData, resolveFacadeId, validateFacadeOptions } from "../charts/shared.js";
|
|
7
|
+
|
|
8
|
+
const COMMON_OPTIONS = ["id", "x", "y", "space", "source", "data", "coordinate", "temporalUnit"];
|
|
9
|
+
const LINE_STYLE = ["stroke", "strokeWidth", "strokeDash", "opacity"];
|
|
10
|
+
const BAND_STYLE = ["fill", "opacity", "stroke", "strokeWidth"];
|
|
11
|
+
|
|
12
|
+
function resolveBinding(program, args, axis, operation, band) {
|
|
13
|
+
const space = args.space === undefined ? "data" : args.space;
|
|
14
|
+
if (space !== "data" && space !== "plot") {
|
|
15
|
+
throw new Error(`${operation} space must be data or plot.`);
|
|
16
|
+
}
|
|
17
|
+
if (space === "plot") {
|
|
18
|
+
if (Object.hasOwn(args, "source") || Object.hasOwn(args, "temporalUnit")) {
|
|
19
|
+
throw new Error(`${operation} plot space does not accept source or temporalUnit.`);
|
|
20
|
+
}
|
|
21
|
+
return { data: resolveFacadeData(program, args.data, operation),
|
|
22
|
+
fieldType: "quantitative", coordinate: args.coordinate, plot: true };
|
|
23
|
+
}
|
|
24
|
+
if (Object.hasOwn(args, "data") || Object.hasOwn(args, "coordinate")) {
|
|
25
|
+
throw new Error(`${operation} data space uses source data and coordinate.`);
|
|
26
|
+
}
|
|
27
|
+
const source = resolveEligibleLayer(program, {
|
|
28
|
+
target: args.source === undefined ? undefined : validateUserId(args.source, `${operation} source`),
|
|
29
|
+
predicate: layer => !isSourceOwnedText(layer) && layer.data !== undefined && layer.encoding?.[axis]?.scale !== undefined &&
|
|
30
|
+
findCoordinate(program, layer.coordinate)?.type === "cartesian" &&
|
|
31
|
+
(!band || ["quantitative", "temporal"].includes(layer.encoding[axis].fieldType)),
|
|
32
|
+
label: `${operation} Cartesian layer`,
|
|
33
|
+
targetOption: "source"
|
|
34
|
+
});
|
|
35
|
+
const encoding = source.encoding[axis];
|
|
36
|
+
return { data: source.data, coordinate: source.coordinate, scale: encoding.scale,
|
|
37
|
+
fieldType: encoding.fieldType, temporalUnit: args.temporalUnit === undefined ? encoding.temporalUnit : args.temporalUnit };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function createReference(program, args, band) {
|
|
41
|
+
const operation = band ? "createReferenceBand" : "createReferenceLine";
|
|
42
|
+
const styleOptions = band ? BAND_STYLE : LINE_STYLE;
|
|
43
|
+
validateFacadeOptions(args, [...COMMON_OPTIONS, ...styleOptions], operation);
|
|
44
|
+
const axes = ["x", "y"].filter(axis => Object.hasOwn(args, axis));
|
|
45
|
+
if (axes.length !== 1) throw new Error(`${operation} requires exactly one of x or y.`);
|
|
46
|
+
const axis = axes[0];
|
|
47
|
+
const values = band ? args[axis] : [args[axis]];
|
|
48
|
+
if (!Array.isArray(values) || values.length !== (band ? 2 : 1)) {
|
|
49
|
+
throw new Error(`${operation} ${axis} requires a two-value interval.`);
|
|
50
|
+
}
|
|
51
|
+
const id = resolveFacadeId(program, args.id, {
|
|
52
|
+
defaultId: band ? "referenceBand" : "referenceLine", operation
|
|
53
|
+
});
|
|
54
|
+
const binding = resolveBinding(program, args, axis, operation, band);
|
|
55
|
+
if (binding.plot && !values.every(value => Number.isFinite(value) && value >= 0 && value <= 1)) {
|
|
56
|
+
throw new Error(`${operation} plot coordinates must be finite numbers in [0, 1].`);
|
|
57
|
+
}
|
|
58
|
+
const scale = binding.plot ? `${id}-${axis}` : binding.scale;
|
|
59
|
+
const style = Object.fromEntries(styleOptions.filter(key => args[key] !== undefined).map(key => [key, args[key]]));
|
|
60
|
+
const encode = axis === "x" ? "encodeX" : "encodeY";
|
|
61
|
+
const options = { target: id, fieldType: binding.fieldType, scale: { id: scale },
|
|
62
|
+
...(binding.coordinate === undefined ? {} : { coordinate: binding.coordinate }),
|
|
63
|
+
...(binding.temporalUnit === undefined ? {} : { temporalUnit: binding.temporalUnit }) };
|
|
64
|
+
const apply = initial => {
|
|
65
|
+
let next = binding.plot
|
|
66
|
+
? initial.createScale({ id: scale, type: "linear", domain: [0, 1] })
|
|
67
|
+
: initial;
|
|
68
|
+
next = band
|
|
69
|
+
? next.createRectMark({ id, data: binding.data, fill: "#94a3b8", opacity: 0.15, stroke: false, ...style })
|
|
70
|
+
: next.createRuleMark({ id, data: binding.data, stroke: "#64748b", strokeWidth: 1, strokeDash: "dashed", ...style });
|
|
71
|
+
next = next[encode]({ ...options, datum: values[0] });
|
|
72
|
+
return band ? next[`${encode}2`]({ ...options, datum: values[1] }) : next;
|
|
73
|
+
};
|
|
74
|
+
// Validate the complete lower-action chain on a discarded immutable branch.
|
|
75
|
+
apply(program);
|
|
76
|
+
return apply(program);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export const createReferenceLine = action(
|
|
80
|
+
{ op: "createReferenceLine", description: "Create a constant reference line in data or plot coordinates." },
|
|
81
|
+
function (args = {}) { return createReference(this, args, false); }
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
export const createReferenceBand = action(
|
|
85
|
+
{ op: "createReferenceBand", description: "Create a constant reference interval in data or plot coordinates." },
|
|
86
|
+
function (args = {}) { return createReference(this, args, true); }
|
|
87
|
+
);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { isSourceOwnedText } from "../../grammar/text.js";
|
|
1
2
|
import { action } from "../../core/action.js";
|
|
2
3
|
import { validateUserId } from "../../core/identifiers.js";
|
|
3
4
|
import { validateKeys } from "../../core/validation.js";
|
|
4
5
|
import { findDataset } from "../../selectors/datasets.js";
|
|
5
6
|
import { findLayer } from "../../selectors/layers.js";
|
|
7
|
+
import { applyDetachedScaleRematerialization } from "../../materialization/dependencies.js";
|
|
6
8
|
import {
|
|
7
9
|
getPositionChannelDefinition,
|
|
8
10
|
POSITION_CHANNELS
|
|
@@ -28,12 +30,17 @@ function ownedChildren(program, id) {
|
|
|
28
30
|
config.boxPlot?.whiskerId,
|
|
29
31
|
config.boxPlot?.medianId,
|
|
30
32
|
config.boxPlot?.outlierId,
|
|
31
|
-
config.gradientPlot?.centerId
|
|
33
|
+
config.gradientPlot?.centerId,
|
|
34
|
+
config.intervalPlot?.intervalId,
|
|
35
|
+
config.endpointPlot?.roles?.stemId,
|
|
36
|
+
config.endpointPlot?.roles?.startId,
|
|
37
|
+
config.endpointPlot?.roles?.connectorId,
|
|
38
|
+
...(config.raincloudPlot?.ownedChildIds ?? [])
|
|
32
39
|
].concat(
|
|
33
40
|
program.semanticSpec.layers
|
|
34
41
|
.filter(layer => layer.source === id)
|
|
35
42
|
.map(layer => layer.id)
|
|
36
|
-
).filter(child => child !== undefined && findLayer(program, child) !== undefined);
|
|
43
|
+
).filter(child => child !== undefined && child !== id && findLayer(program, child) !== undefined);
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
function ownership(program) {
|
|
@@ -90,7 +97,10 @@ function ownedDerivedData(program, ids) {
|
|
|
90
97
|
config.regression?.dataId,
|
|
91
98
|
config.boxPlot?.summaryId,
|
|
92
99
|
config.boxPlot?.outlierDataId,
|
|
93
|
-
config.gradientPlot?.profileId
|
|
100
|
+
config.gradientPlot?.profileId,
|
|
101
|
+
config.violinPlot?.materialized === true ? layer?.data : undefined,
|
|
102
|
+
config.ecdfPlot?.data,
|
|
103
|
+
config.endpointPlot?.data
|
|
94
104
|
]) {
|
|
95
105
|
if (candidate !== undefined) candidates.add(candidate);
|
|
96
106
|
}
|
|
@@ -161,7 +171,7 @@ export function removeOwnedMark(program, id, partial = false) {
|
|
|
161
171
|
|
|
162
172
|
function hasScaleConsumer(program, channel, scale) {
|
|
163
173
|
return program.semanticSpec.layers.some(
|
|
164
|
-
layer => layer.encoding?.[channel]?.scale === scale
|
|
174
|
+
layer => !isSourceOwnedText(layer) && layer.encoding?.[channel]?.scale === scale
|
|
165
175
|
);
|
|
166
176
|
}
|
|
167
177
|
|
|
@@ -198,6 +208,7 @@ export const removeMark = action(
|
|
|
198
208
|
validateKeys(args, OPTIONS, "removeMark");
|
|
199
209
|
const owner = resolveOwner(this, args.target);
|
|
200
210
|
const ids = collectClosure(this, owner.id);
|
|
211
|
+
const previousLayers = ids.map(id => findLayer(this, id));
|
|
201
212
|
const derived = ownedDerivedData(this, ids);
|
|
202
213
|
const positionScales = usedPositionScales(this, ids);
|
|
203
214
|
let next = this;
|
|
@@ -227,6 +238,7 @@ export const removeMark = action(
|
|
|
227
238
|
}
|
|
228
239
|
next = next._withoutMaterializationConfig(["marks", id]);
|
|
229
240
|
next = next._withoutMaterializationConfig(["jitters", id]);
|
|
241
|
+
next = next._withoutMaterializationConfig(["pointPacking", id]);
|
|
230
242
|
next = next._withoutMaterializationConfig(["labelLayouts", id]);
|
|
231
243
|
}
|
|
232
244
|
|
|
@@ -236,6 +248,7 @@ export const removeMark = action(
|
|
|
236
248
|
next = next.releaseDerivedData({ id });
|
|
237
249
|
}
|
|
238
250
|
}
|
|
251
|
+
next = applyDetachedScaleRematerialization(next, previousLayers);
|
|
239
252
|
return next._withContext({
|
|
240
253
|
...(ids.includes(this.context.currentMark) ? { currentMark: undefined } : {}),
|
|
241
254
|
...(selectionCleanup.selectionIds.includes(this.context.currentSelection)
|
|
@@ -3,6 +3,7 @@ import { validateUserId } from "../../../core/identifiers.js";
|
|
|
3
3
|
import { validatePositiveFinite } from "../../../core/validation.js";
|
|
4
4
|
import {
|
|
5
5
|
mapContinuousScaleValues,
|
|
6
|
+
mapOrdinalOffsetValues,
|
|
6
7
|
mapOrdinalPositionValues,
|
|
7
8
|
mapOrdinalValues,
|
|
8
9
|
normalizeStrokeDashPattern
|
|
@@ -10,7 +11,8 @@ import {
|
|
|
10
11
|
import { deriveRuleValues, resolveRuleMode } from "../../../grammar/rules.js";
|
|
11
12
|
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
12
13
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
13
|
-
import { findLayer } from "../../../selectors/layers.js";
|
|
14
|
+
import { findLayer, resolveEligibleLayer } from "../../../selectors/layers.js";
|
|
15
|
+
import { applyRuleAppearance, planRuleAppearance } from "./appearance.js";
|
|
14
16
|
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
15
17
|
import {
|
|
16
18
|
assertMarkAvailable,
|
|
@@ -25,8 +27,13 @@ import {
|
|
|
25
27
|
import { resolveMarkGraphicPlacement } from
|
|
26
28
|
"../../../materialization/graphicHierarchy.js";
|
|
27
29
|
import { rematerializeHighlightBaseline } from "../lifecycle.js";
|
|
30
|
+
import { applyOffsetPositionValues } from
|
|
31
|
+
"../../../materialization/rowEncoding.js";
|
|
32
|
+
import { resolveCategorySlotOffset } from
|
|
33
|
+
"../../../materialization/categorySlotOffset.js";
|
|
28
34
|
|
|
29
|
-
const CREATE_OPTIONS = Object.freeze(["id", "data"]);
|
|
35
|
+
const CREATE_OPTIONS = Object.freeze(["id", "data", "stroke", "strokeWidth", "strokeDash", "opacity"]);
|
|
36
|
+
const EDIT_OPTIONS = Object.freeze(["target", "stroke", "strokeWidth", "strokeDash", "opacity"]);
|
|
30
37
|
const REMATERIALIZE_OPTIONS = Object.freeze(["id"]);
|
|
31
38
|
const SPAN_OPTIONS = Object.freeze(["id", "orientation", "size"]);
|
|
32
39
|
const DEFAULT_RULE_CONFIG = Object.freeze({
|
|
@@ -93,6 +100,7 @@ const createRuleMark = action(
|
|
|
93
100
|
inherited?.data !== undefined ? { data: inherited.data } : {})
|
|
94
101
|
});
|
|
95
102
|
assertMarkAvailable(this, id);
|
|
103
|
+
const appearance = planRuleAppearance(args, inherited);
|
|
96
104
|
|
|
97
105
|
let created = this
|
|
98
106
|
.editSemantic({ property: `layer[${id}].mark.type`, value: "rule" })
|
|
@@ -116,7 +124,23 @@ const createRuleMark = action(
|
|
|
116
124
|
}
|
|
117
125
|
})
|
|
118
126
|
});
|
|
119
|
-
return materializeInheritedMark(created, id);
|
|
127
|
+
return applyRuleAppearance(materializeInheritedMark(created, id), id, appearance);
|
|
128
|
+
}
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
const editRuleMark = action(
|
|
132
|
+
{ op: "editRuleMark", description: "Edit constant rule appearance through its encoding owners." },
|
|
133
|
+
function (args = {}) {
|
|
134
|
+
validateMarkOptions(args, EDIT_OPTIONS, "editRuleMark");
|
|
135
|
+
if (args.target !== undefined) validateUserId(args.target, "Rule mark id");
|
|
136
|
+
const layer = resolveEligibleLayer(this, {
|
|
137
|
+
target: args.target,
|
|
138
|
+
predicate: candidate => candidate.mark?.type === "rule",
|
|
139
|
+
label: "rule mark"
|
|
140
|
+
});
|
|
141
|
+
const appearance = planRuleAppearance(args, layer);
|
|
142
|
+
if (appearance.length === 0) throw new Error("editRuleMark requires an appearance change.");
|
|
143
|
+
return applyRuleAppearance(this, layer.id, appearance);
|
|
120
144
|
}
|
|
121
145
|
);
|
|
122
146
|
|
|
@@ -154,7 +178,10 @@ const rematerializeRuleMark = action(
|
|
|
154
178
|
}
|
|
155
179
|
|
|
156
180
|
const scaleIds = [...new Set(
|
|
157
|
-
[
|
|
181
|
+
[
|
|
182
|
+
"x", "y", "x2", "y2", "xOffset", "yOffset",
|
|
183
|
+
"strokeDash", "strokeWidth", "opacity"
|
|
184
|
+
]
|
|
158
185
|
.map(channel => layer.encoding?.[channel]?.scale)
|
|
159
186
|
.filter(scale => scale !== undefined)
|
|
160
187
|
)];
|
|
@@ -178,6 +205,25 @@ const rematerializeRuleMark = action(
|
|
|
178
205
|
resolved.resolvedScales[encoding.scale]
|
|
179
206
|
);
|
|
180
207
|
}
|
|
208
|
+
for (const [channel, offsetChannel] of [
|
|
209
|
+
["x", "xOffset"],
|
|
210
|
+
["y", "yOffset"]
|
|
211
|
+
]) {
|
|
212
|
+
const encoding = layer.encoding?.[offsetChannel];
|
|
213
|
+
if (encoding === undefined || mapped[channel] === undefined) continue;
|
|
214
|
+
const offsetScale = resolved.resolvedScales[encoding.scale];
|
|
215
|
+
const offset = mapOrdinalOffsetValues(
|
|
216
|
+
derived.values[offsetChannel],
|
|
217
|
+
offsetScale
|
|
218
|
+
);
|
|
219
|
+
mapped[channel] = applyOffsetPositionValues({
|
|
220
|
+
base: mapped[channel],
|
|
221
|
+
offset,
|
|
222
|
+
parentScale: resolved.resolvedScales[layer.encoding[channel].scale],
|
|
223
|
+
offsetScale,
|
|
224
|
+
channel
|
|
225
|
+
});
|
|
226
|
+
}
|
|
181
227
|
const bounds = resolveGraphicBounds(resolved);
|
|
182
228
|
const x1 = [];
|
|
183
229
|
const y1 = [];
|
|
@@ -238,6 +284,15 @@ const rematerializeRuleMark = action(
|
|
|
238
284
|
x2.push(endX);
|
|
239
285
|
y2.push(endY);
|
|
240
286
|
}
|
|
287
|
+
const categoryOffset = resolveCategorySlotOffset(resolved, layer);
|
|
288
|
+
if (categoryOffset?.pixels) {
|
|
289
|
+
const targets = categoryOffset.channel === "x" ? [x1, x2] : [y1, y2];
|
|
290
|
+
for (const values of targets) {
|
|
291
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
292
|
+
if (Number.isFinite(values[index])) values[index] += categoryOffset.pixels;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
241
296
|
|
|
242
297
|
const resolvedConfig = {
|
|
243
298
|
...DEFAULT_RULE_CONFIG,
|
|
@@ -312,6 +367,7 @@ export const materializeRuleSpan = action(
|
|
|
312
367
|
export function registerRuleMarkActions(ProgramClass) {
|
|
313
368
|
Object.assign(ProgramClass.prototype, {
|
|
314
369
|
createRuleMark,
|
|
370
|
+
editRuleMark,
|
|
315
371
|
rematerializeRuleMark,
|
|
316
372
|
materializeRuleSpan
|
|
317
373
|
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { validateRuleStroke, validateRuleStrokeWidth } from "../../../grammar/ruleAppearance.js";
|
|
2
|
+
import { normalizeStrokeDashPattern, validateOpacityValue } from "../../../grammar/scales/index.js";
|
|
3
|
+
|
|
4
|
+
const STYLE_ACTIONS = Object.freeze({
|
|
5
|
+
stroke: ["encodeStroke", validateRuleStroke],
|
|
6
|
+
strokeWidth: ["encodeStrokeWidth", validateRuleStrokeWidth],
|
|
7
|
+
strokeDash: ["encodeStrokeDash", normalizeStrokeDashPattern],
|
|
8
|
+
opacity: ["encodeOpacity", validateOpacityValue]
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
export function planRuleAppearance(args, layer) {
|
|
12
|
+
return Object.entries(STYLE_ACTIONS).flatMap(([property, [op, validate]]) => {
|
|
13
|
+
if (!Object.hasOwn(args, property)) return [];
|
|
14
|
+
if (layer?.encoding?.[property]?.field !== undefined) {
|
|
15
|
+
throw new Error(`Rule ${property} conflicts with a field encoding; use ${op} with value to replace it.`);
|
|
16
|
+
}
|
|
17
|
+
validate(args[property], `Rule ${property}`);
|
|
18
|
+
return [{ op, value: args[property] }];
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function applyRuleAppearance(program, target, plan) {
|
|
23
|
+
let next = program;
|
|
24
|
+
for (const { op, value } of plan) next = next[op]({ target, value });
|
|
25
|
+
return next;
|
|
26
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { isSourceOwnedText } from "../../grammar/text.js";
|
|
1
2
|
import {
|
|
2
3
|
resolveOptionalUserId,
|
|
3
4
|
validateUserId
|
|
4
5
|
} from "../../core/identifiers.js";
|
|
5
|
-
import { findDataset } from "../../selectors/datasets.js";
|
|
6
|
+
import { findDataset, requireMaterializedDataset } from "../../selectors/datasets.js";
|
|
6
7
|
import { findLayer, hasLayer } from "../../selectors/layers.js";
|
|
7
8
|
import { findSemanticScale } from "../../selectors/scales.js";
|
|
8
9
|
import { resolveMarkPositionPolicy } from "../encodings/position/policies/index.js";
|
|
@@ -27,12 +28,7 @@ export function resolveMarkData(program, requested) {
|
|
|
27
28
|
throw new Error("Mark creation requires data or a current dataset.");
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const dataset =
|
|
31
|
-
|
|
32
|
-
if (dataset === undefined) {
|
|
33
|
-
throw new Error(`Unknown dataset "${data}".`);
|
|
34
|
-
}
|
|
35
|
-
|
|
31
|
+
const dataset = requireMaterializedDataset(program, data);
|
|
36
32
|
return { data, dataset };
|
|
37
33
|
}
|
|
38
34
|
|
|
@@ -60,7 +56,7 @@ export function assertMarkAvailable(program, id) {
|
|
|
60
56
|
throw new Error(`Mark "${id}" already exists.`);
|
|
61
57
|
}
|
|
62
58
|
|
|
63
|
-
if (program.graphicSpec.objects
|
|
59
|
+
if (Object.hasOwn(program.graphicSpec.objects, id)) {
|
|
64
60
|
throw new Error(`Graphic "${id}" already exists.`);
|
|
65
61
|
}
|
|
66
62
|
}
|
|
@@ -78,9 +74,9 @@ const POSITION_POLICY_SUBSETS = Object.freeze([
|
|
|
78
74
|
]);
|
|
79
75
|
|
|
80
76
|
function inheritedPositionEncodings(encoding) {
|
|
81
|
-
if (encoding?.field === undefined) return undefined;
|
|
77
|
+
if (encoding?.field === undefined && encoding?.aggregate !== "count") return undefined;
|
|
82
78
|
const base = Object.fromEntries(
|
|
83
|
-
["field", "fieldType", "scale", "title"]
|
|
79
|
+
["field", "fieldType", "scale", "title", "temporalUnit"]
|
|
84
80
|
.filter(property => Object.hasOwn(encoding, property))
|
|
85
81
|
.map(property => [property, encoding[property]])
|
|
86
82
|
);
|
|
@@ -104,6 +100,9 @@ function inheritedPositionEncodings(encoding) {
|
|
|
104
100
|
function scaleSupportsEncoding(program, markType, channel, encoding) {
|
|
105
101
|
const scale = findSemanticScale(program, encoding.scale);
|
|
106
102
|
if (scale === undefined) return false;
|
|
103
|
+
if (scale.radialMapping !== undefined) {
|
|
104
|
+
return markType === "arc" && channel === "radius" && ["count", "sum"].includes(encoding.aggregate);
|
|
105
|
+
}
|
|
107
106
|
const categorical = ["nominal", "ordinal"].includes(encoding.fieldType);
|
|
108
107
|
if (categorical) {
|
|
109
108
|
if (["bar", "rect"].includes(markType)) return scale.type === "band";
|
|
@@ -143,7 +142,8 @@ export function resolveCompatibleEncodings(program, source, markType) {
|
|
|
143
142
|
layer: candidate,
|
|
144
143
|
dataset,
|
|
145
144
|
channel,
|
|
146
|
-
args: encoding,
|
|
145
|
+
args: { ...encoding, ...(channel === "radius" && findSemanticScale(program, encoding.scale)?.radialMapping !== undefined
|
|
146
|
+
? { mapping: findSemanticScale(program, encoding.scale).radialMapping } : {}) },
|
|
147
147
|
field: encoding.field,
|
|
148
148
|
fieldType: encoding.fieldType
|
|
149
149
|
});
|
|
@@ -163,7 +163,7 @@ export function resolveCompatibleEncodings(program, source, markType) {
|
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
function eligibleLayeredSource(program, layer, requestedData, markType) {
|
|
166
|
-
if (layer?.data === undefined) return false;
|
|
166
|
+
if (layer?.data === undefined || isSourceOwnedText(layer)) return false;
|
|
167
167
|
if (requestedData !== undefined && layer.data !== requestedData) return false;
|
|
168
168
|
return Object.keys(resolveCompatibleEncodings(program, layer, markType)).length > 0;
|
|
169
169
|
}
|