ggaction 0.0.21 → 0.0.22
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 +73 -0
- package/README.md +3 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +6 -2
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +1 -0
- package/src/actions/boxPlots/components.js +1 -96
- package/src/actions/boxPlots/create.js +6 -1
- package/src/actions/boxPlots/edit.js +8 -0
- package/src/actions/boxPlots/materialize.js +3 -0
- package/src/actions/boxPlots/options.js +1 -1
- package/src/actions/canvas/actions.js +2 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +2 -1
- package/src/actions/canvas/plotOrder.js +14 -0
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +4 -4
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +1 -1
- package/src/actions/charts/endpoints.js +54 -5
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/histogram.js +1 -1
- package/src/actions/charts/horizon.js +1 -1
- package/src/actions/charts/index.js +2 -1
- package/src/actions/charts/interval-regression.js +1 -1
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/radar.js +63 -3
- package/src/actions/charts/raincloud.js +37 -6
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +3 -3
- package/src/actions/charts/text.js +1 -1
- package/src/actions/composition/actions.js +10 -3
- package/src/actions/coordinates/actions.js +2 -0
- package/src/actions/data/basic.js +6 -0
- package/src/actions/data/derived.js +6 -2
- package/src/actions/data/edit.js +21 -35
- package/src/actions/data/filter.js +1 -376
- package/src/actions/data/flatten.js +19 -0
- package/src/actions/data/index.js +6 -0
- package/src/actions/data/lookup.js +50 -0
- package/src/actions/data/raincloudFilter.js +56 -0
- package/src/actions/data/shared.js +10 -0
- package/src/actions/distributions/revision.js +3 -1
- package/src/actions/encodings/angle.js +2 -1
- package/src/actions/encodings/appearance.js +38 -13
- package/src/actions/encodings/channels.js +14 -3
- package/src/actions/encodings/color/continuous.js +26 -10
- package/src/actions/encodings/color/index.js +50 -20
- package/src/actions/encodings/color/policy.js +57 -4
- package/src/actions/encodings/parallel.js +4 -0
- package/src/actions/encodings/position/policies/bar.js +10 -6
- package/src/actions/encodings/position/policies/line.js +1 -152
- package/src/actions/encodings/position/resolve.js +4 -1
- package/src/actions/encodings/ranged.js +46 -0
- package/src/actions/encodings/remove.js +1 -1
- package/src/actions/encodings/ruleAppearance.js +7 -6
- package/src/actions/encodings/shared.js +14 -2
- package/src/actions/encodings/stroke.js +20 -14
- package/src/actions/encodings/strokeDash.js +9 -11
- package/src/actions/encodings/text.js +30 -2
- package/src/actions/errorBars/edit.js +24 -4
- package/src/actions/facets/actions.js +1 -793
- package/src/actions/facets/derive.js +34 -5
- package/src/actions/facets/replay.js +1 -38
- package/src/actions/gradientPlots/components.js +21 -9
- package/src/actions/gradientPlots/create.js +4 -2
- package/src/actions/gradientPlots/edit.js +18 -1
- package/src/actions/gradientPlots/materialize.js +35 -36
- package/src/actions/gradientPlots/options.js +1 -1
- package/src/actions/gradientPlots/paint.js +13 -7
- package/src/actions/guides/applicability.js +2 -0
- package/src/actions/guides/axes/axes.js +39 -0
- package/src/actions/guides/axes/axis.js +9 -5
- package/src/actions/guides/axes/edit.js +11 -8
- package/src/actions/guides/axes/labels.js +24 -8
- package/src/actions/guides/axes/parallel/policy.js +3 -5
- package/src/actions/guides/axes/parallel/resolve.js +4 -3
- package/src/actions/guides/axes/policy.js +22 -0
- package/src/actions/guides/axes/tickGroups.js +4 -2
- package/src/actions/guides/axes/ticks.js +22 -4
- package/src/actions/guides/axes/titles.js +19 -6
- package/src/actions/guides/facadeAxes.js +3 -0
- package/src/actions/guides/grids/grid.js +3 -1
- package/src/actions/guides/grids/resolve.js +7 -11
- package/src/actions/guides/guides.js +21 -1
- package/src/actions/guides/legends/categorical/actions.js +1 -515
- package/src/actions/guides/legends/categorical/layout.js +5 -2
- package/src/actions/guides/legends/categorical/options.js +4 -0
- package/src/actions/guides/legends/categorical/resolve.js +8 -3
- package/src/actions/guides/legends/categorical/symbols.js +3 -3
- package/src/actions/guides/legends/colorSize.js +2 -0
- package/src/actions/guides/legends/continuous/common.js +1 -1
- package/src/actions/guides/legends/continuous/gradient.js +1 -360
- package/src/actions/guides/legends/continuous/interval.js +1 -1
- package/src/actions/guides/legends/continuous/opacity.js +15 -1
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +1 -1
- package/src/actions/guides/legends/lane.js +1 -1
- package/src/actions/guides/legends/sampling.js +1 -140
- package/src/actions/guides/legends/strokeWidth.js +3 -2
- package/src/actions/guides/legends/transition.js +19 -2
- package/src/actions/guides/polar/axes/facade.js +5 -4
- package/src/actions/guides/polar/axes/labels.js +5 -4
- package/src/actions/guides/polar/axes/titles.js +5 -4
- package/src/actions/guides/polar/resolve.js +2 -1
- package/src/actions/guides/tickValues.js +7 -7
- package/src/actions/marks/arc/actions.js +5 -7
- package/src/actions/marks/area/actions.js +9 -3
- package/src/actions/marks/bar/create.js +4 -2
- package/src/actions/marks/bar/edit.js +9 -6
- package/src/actions/marks/bar/materialize.js +5 -3
- package/src/actions/marks/dependentReferences.js +87 -0
- package/src/actions/marks/index.js +2 -0
- package/src/actions/marks/lifecycle.js +2 -1
- package/src/actions/marks/line/actions.js +24 -6
- package/src/actions/marks/line/materialize.js +7 -3
- package/src/actions/marks/point/create.js +5 -4
- package/src/actions/marks/point/edit.js +6 -3
- package/src/actions/marks/point/jitter.js +6 -6
- package/src/actions/marks/point/materialize.js +36 -39
- package/src/actions/marks/rect/actions.js +16 -4
- package/src/actions/marks/references.js +183 -35
- package/src/actions/marks/rule/actions.js +20 -9
- package/src/actions/marks/shared.js +2 -1
- package/src/actions/marks/text/actions.js +10 -3
- package/src/actions/marks/tick/actions.js +11 -2
- package/src/actions/primitives/editGraphics.js +28 -3
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semanticAction.js +13 -7
- package/src/actions/primitives/semanticValidation/layer.js +20 -0
- package/src/actions/primitives/semanticValidation/scale.js +8 -1
- package/src/actions/scales/channels.js +1 -1
- package/src/actions/scales/consumers/common.js +19 -1
- package/src/actions/scales/consumers/families.js +2 -2
- package/src/actions/scales/consumers/seriesLayout.js +4 -3
- package/src/actions/scales/edit.js +11 -0
- package/src/actions/scales/editPolicy.js +5 -3
- package/src/actions/scales/preview.js +2 -1
- package/src/actions/scales/quantitativeColor.js +2 -4
- package/src/actions/selection/actions.js +10 -1
- package/src/actions/theme/reconcile.js +37 -9
- package/src/core/compositionState.js +21 -4
- package/src/core/datasetReferences.js +17 -0
- package/src/core/immutable.js +1 -0
- package/src/core/resourceReferences.js +5 -3
- package/src/core/validation.js +4 -0
- package/src/core/vocabulary.js +24 -2
- package/src/grammar/aggregate.js +1 -0
- package/src/grammar/arcs.js +1 -1
- package/src/grammar/areaSeries.js +1 -1
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bars/policy.js +2 -2
- package/src/grammar/datasetDependencies.js +97 -0
- package/src/grammar/datasetSchema.js +31 -1
- package/src/grammar/facetHeaders.js +1 -0
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/legendIntent.js +6 -2
- package/src/grammar/facets/scales.js +27 -7
- package/src/grammar/flatten.js +85 -0
- package/src/grammar/histogram.js +13 -1
- package/src/grammar/horizon.js +1 -494
- package/src/grammar/jitter.js +2 -2
- package/src/grammar/legendSpacing.js +45 -0
- package/src/grammar/lineMissing.js +90 -0
- package/src/grammar/lineSeries.js +1 -376
- package/src/grammar/lookup.js +97 -0
- package/src/grammar/markFilter.js +4 -2
- package/src/grammar/markLabels.js +8 -3
- package/src/grammar/numeric.js +1 -1
- package/src/grammar/pathCommands.js +1 -1
- package/src/grammar/pathSeries.js +1 -109
- package/src/grammar/pointPolygon.js +81 -0
- package/src/grammar/pointShapes.js +15 -15
- package/src/grammar/rects.js +1 -1
- package/src/grammar/regression/derive.js +1 -195
- package/src/grammar/scales/appearance.js +1 -1
- package/src/grammar/scales/color.js +1 -1
- package/src/grammar/scales/colorConsumers.js +1 -63
- package/src/grammar/scales/continuous.js +58 -1
- package/src/grammar/scales/definition.js +1 -1
- package/src/grammar/scales/ordinal.js +5 -2
- package/src/grammar/scales/policies.js +3 -3
- package/src/grammar/schemas/graphicBounds.js +40 -12
- package/src/grammar/schemas/graphicClip.js +23 -0
- package/src/grammar/schemas/graphicTree.js +16 -0
- package/src/grammar/statisticalReference.js +22 -0
- package/src/grammar/text.js +65 -1
- package/src/grammar/ticks.js +92 -1
- package/src/grammar/transformTopology.js +3 -1
- package/src/grammar/transforms.js +24 -0
- package/src/inspection.js +1 -495
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/canvas.js +1 -1
- package/src/layout/facetContentFlow.js +101 -0
- package/src/layout/facetHeaderTitles.js +1 -0
- package/src/layout/facetTopLane.js +29 -0
- package/src/layout/facets.js +1 -1
- package/src/layout/legendGroup.js +1 -1
- package/src/layout/legendItems.js +26 -10
- package/src/layout/legendSpacing.js +66 -0
- package/src/layout/plotDrawOrder.js +17 -0
- package/src/materialization/bars/histogram.js +8 -2
- package/src/materialization/bars/resolve.js +1 -1
- package/src/materialization/composition.js +1 -0
- package/src/materialization/compositionSnapshot.js +22 -2
- package/src/materialization/dataProvenance.js +1 -1
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +23 -11
- package/src/materialization/facetGuides/preparation.js +43 -24
- package/src/materialization/facetHeaders.js +1 -1
- package/src/materialization/facetTopLane.js +25 -0
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +5 -0
- package/src/materialization/graphicTranslation.js +2 -0
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/legends.js +1 -1
- package/src/materialization/markClip.js +26 -0
- package/src/materialization/marks/capabilities.js +3 -2
- package/src/materialization/marks/grouping.js +7 -5
- package/src/materialization/marks/index.js +4 -2
- package/src/materialization/marks/policies.js +4 -0
- package/src/materialization/plotDrawOrder.js +44 -0
- package/src/materialization/rect.js +11 -3
- package/src/materialization/scaleGuideDependencies.js +13 -5
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/selection/items/path.js +1 -0
- package/src/materialization/selection/policies/index.js +2 -1
- package/src/materialization/text.js +10 -5
- package/src/mcp/cli.js +0 -0
- package/src/persistence/validation.js +108 -18
- package/src/persistence.js +4 -2
- package/src/renderers/canvas/index.js +16 -0
- package/src/renderers/canvas/isolated.js +11 -1
- package/src/renderers/canvas/native.js +7 -0
- package/src/renderers/canvas/scene.js +66 -0
- package/src/renderers/canvas/text.js +3 -2
- package/src/renderers/svg.js +1 -617
- package/src/renderers/svgSerialization.js +643 -0
- package/src/renderers/transitionScene.js +67 -0
- package/src/scene.js +89 -0
- package/src/selectors/axes.js +1 -1
- package/src/selectors/datasets.js +1 -0
- package/src/selectors/grids.js +9 -0
- package/src/selectors/markClip.js +28 -0
- package/src/selectors/raincloud.js +7 -0
- package/src/version.js +1 -1
- package/types/basic.d.ts +7 -0
- package/types/index.d.ts +8 -0
- package/types/program.d.ts +270 -60
- package/types/scene.d.ts +35 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { axisTitleAllowsLabelOverlap } from "../../../grammar/axisTextPlacement.js";
|
|
1
2
|
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
3
|
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
4
|
import { axisThemeTypography } from "../../theme/state.js";
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
formatTransformedTick,
|
|
22
23
|
mapOrdinalPositionValues
|
|
23
24
|
} from "../../../grammar/scales/index.js";
|
|
24
|
-
import { formatTimeTick, formatTimeTicks } from "../../../grammar/ticks.js";
|
|
25
|
+
import { formatTimeTick, formatTimeTicks, validateTickRounding } from "../../../grammar/ticks.js";
|
|
25
26
|
import { resolveRotation } from "../../../grammar/rotation.js";
|
|
26
27
|
import { valuesFromTickConfig } from "../tickValues.js";
|
|
27
28
|
import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
@@ -29,6 +30,7 @@ import { AXIS_LABEL_FONT_SIZES, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
|
29
30
|
import {
|
|
30
31
|
defaultAxisPosition,
|
|
31
32
|
formatAxisValue,
|
|
33
|
+
formatNumericAxisTicks,
|
|
32
34
|
resolveAxisLabelGeometry,
|
|
33
35
|
validateAxisFormat,
|
|
34
36
|
validateAxisPosition
|
|
@@ -51,7 +53,7 @@ import {
|
|
|
51
53
|
} from "../../../grammar/displayLabels.js";
|
|
52
54
|
|
|
53
55
|
const OPTIONS = [
|
|
54
|
-
"target", "scale", "position", "count", "values", "offset", "format", "color",
|
|
56
|
+
"target", "scale", "position", "count", "values", "rounding", "offset", "format", "color",
|
|
55
57
|
"fontSize", "fontFamily", "fontWeight", "rotation", "maxWidth", "wrap",
|
|
56
58
|
"lineHeight", "overlap", "labelMap", "alongOffset", "flush", "colorMap", "align"
|
|
57
59
|
];
|
|
@@ -88,6 +90,10 @@ function normalizeLabelLayout(config, args, operation) {
|
|
|
88
90
|
export { validateTextStyle as validateAxisTextStyle };
|
|
89
91
|
|
|
90
92
|
function validateOptions(args, operation, create) {
|
|
93
|
+
validateTickRounding(args.rounding, !Object.hasOwn(args, "rounding"));
|
|
94
|
+
if (Object.hasOwn(args, "rounding") && Object.hasOwn(args, "values")) {
|
|
95
|
+
throw new Error(`${operation} cannot use rounding and values together.`);
|
|
96
|
+
}
|
|
91
97
|
validateAxisTextPlacement(args);
|
|
92
98
|
validateOptionObject(
|
|
93
99
|
args,
|
|
@@ -173,6 +179,9 @@ function assertTickCompatibility(ticks, config, operation) {
|
|
|
173
179
|
if (!ticks) return;
|
|
174
180
|
if (ticks.scale !== config.scale || ticks.mode !== config.mode) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
175
181
|
if (config.mode === "count" && ticks.count !== config.count) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
182
|
+
if (config.mode === "count" && (ticks.rounding ?? "ceiling") !== (config.rounding ?? "ceiling")) {
|
|
183
|
+
throw new Error(`${operation} conflicts with axis tick rounding.`);
|
|
184
|
+
}
|
|
176
185
|
if (config.mode === "values" && !sameOrderedValues(ticks.values, config.values)) throw new Error(`${operation} conflicts with axis ticks.`);
|
|
177
186
|
}
|
|
178
187
|
|
|
@@ -261,12 +270,14 @@ function resolve(program, channel, config, axisId = channel) {
|
|
|
261
270
|
? formatTransformedTick(scale.type, item)
|
|
262
271
|
: String(item)
|
|
263
272
|
);
|
|
264
|
-
const
|
|
265
|
-
?
|
|
266
|
-
:
|
|
273
|
+
const automaticLabels = config.format === "ticks"
|
|
274
|
+
? formatNumericAxisTicks(values, scale.type)
|
|
275
|
+
: scale.type === "time" && config.format === "auto"
|
|
276
|
+
? formatTimeTicks(values, scale.domain)
|
|
277
|
+
: undefined;
|
|
267
278
|
let text = values.map((value, index) =>
|
|
268
279
|
resolveDisplayLabel(value, config.labelMap, raw =>
|
|
269
|
-
|
|
280
|
+
automaticLabels?.[index] ?? fallback(raw)));
|
|
270
281
|
// Empty mapped text suppresses the label, never its raw tick or scale value.
|
|
271
282
|
// Filter all aligned arrays before wrapping so group/color indices stay dense.
|
|
272
283
|
if (text.includes("")) {
|
|
@@ -378,7 +389,7 @@ function resolve(program, channel, config, axisId = channel) {
|
|
|
378
389
|
const title = program.graphicSpec.objects[axisGraphicIds(channel, axisId)[3]]
|
|
379
390
|
? resolveConcreteGraphicBounds(program.graphicSpec, axisGraphicIds(channel, axisId)[3], program.materializationConfigs.textMetrics)
|
|
380
391
|
: undefined;
|
|
381
|
-
if (title &&
|
|
392
|
+
if (title && !axisTitleAllowsLabelOverlap(program.guideConfigs.axis?.[axisId]?.title) &&
|
|
382
393
|
program.guideConfigs.axis?.[axisId]?.title?.inferredOffset !== true &&
|
|
383
394
|
labelBounds.some(item => textBoundsIntersect(item, title))) {
|
|
384
395
|
throw new Error(`The ${channel}-axis labels overlap the axis title.`);
|
|
@@ -413,7 +424,10 @@ function makeEdit(channel) {
|
|
|
413
424
|
mode
|
|
414
425
|
}, args, op);
|
|
415
426
|
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
416
|
-
if (mode === "values")
|
|
427
|
+
if (mode === "values") {
|
|
428
|
+
delete config.count;
|
|
429
|
+
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
430
|
+
} else delete config.values;
|
|
417
431
|
validateConfig(channel, config);
|
|
418
432
|
assertTickCompatibility(this.guideConfigs.axis?.[axisId]?.ticks, config, op);
|
|
419
433
|
const resolved = resolve(this, channel, config, axisId);
|
|
@@ -485,6 +499,7 @@ function makeCreate(channel) {
|
|
|
485
499
|
...axisThemeTypography(this.materializationConfigs.theme, "labels"),
|
|
486
500
|
rotation: DEFAULTS.rotation,
|
|
487
501
|
overlap: DEFAULTS.overlap,
|
|
502
|
+
...(ticks?.rounding === undefined ? {} : { rounding: ticks.rounding }),
|
|
488
503
|
...args,
|
|
489
504
|
inferredValues: !hasValues && !hasCount &&
|
|
490
505
|
(ticks?.inferredValues === true || inferredDiscreteValues !== undefined),
|
|
@@ -493,6 +508,7 @@ function makeCreate(channel) {
|
|
|
493
508
|
config = normalizeAxisLabelMaps(this, config, args, op);
|
|
494
509
|
if (mode === "values") {
|
|
495
510
|
config.values ??= ticks?.values ?? inferredDiscreteValues;
|
|
511
|
+
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
496
512
|
} else {
|
|
497
513
|
config.count ??= ticks?.count ?? DEFAULTS.count;
|
|
498
514
|
}
|
|
@@ -8,12 +8,10 @@ import {
|
|
|
8
8
|
import { AXIS_LABEL_FONT_SIZES, AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../../../theme/defaults.js";
|
|
9
9
|
import { validateAxisTextStyle } from "../labels.js";
|
|
10
10
|
import { validateAxisFormat } from "../policy.js";
|
|
11
|
+
import { axisGraphicIds } from "../../../../selectors/axes.js";
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
line: "parallelAxisLines", ticks: "parallelAxisTicks",
|
|
15
|
-
labels: "parallelAxisLabels", title: "parallelAxisTitles"
|
|
16
|
-
});
|
|
13
|
+
import { PARALLEL_AXIS_PARTS, PARALLEL_AXIS_GRAPHICS } from "../../../../selectors/axes.js";
|
|
14
|
+
export { PARALLEL_AXIS_PARTS, PARALLEL_AXIS_GRAPHICS };
|
|
17
15
|
const STYLE_OPTIONS = Object.freeze({
|
|
18
16
|
line: ["color", "lineWidth"],
|
|
19
17
|
ticks: ["count", "values", "length", "color", "lineWidth"],
|
|
@@ -9,7 +9,7 @@ import { niceTicks } from "../../../../grammar/ticks.js";
|
|
|
9
9
|
import { resolveCoordinateBounds } from "../../../../materialization/coordinateBounds.js";
|
|
10
10
|
import { findCoordinate } from "../../../../selectors/coordinates.js";
|
|
11
11
|
import { findLayer } from "../../../../selectors/layers.js";
|
|
12
|
-
import { formatAxisValue } from "../policy.js";
|
|
12
|
+
import { formatAxisValue, formatNumericAxisTicks } from "../policy.js";
|
|
13
13
|
import { resolveParallelAxisConfigs } from "./policy.js";
|
|
14
14
|
|
|
15
15
|
export function requireParallelAxisLayer(program, target) {
|
|
@@ -79,8 +79,9 @@ function componentValues(program, dimension, config) {
|
|
|
79
79
|
return {
|
|
80
80
|
values,
|
|
81
81
|
y: discrete ? mapOrdinalPositionValues(values, scale) : mapContinuousScaleValues(values, scale),
|
|
82
|
-
text: config.format === undefined ? undefined
|
|
83
|
-
|
|
82
|
+
text: config.format === undefined ? undefined
|
|
83
|
+
: config.format === "ticks" ? formatNumericAxisTicks(values, scale.type)
|
|
84
|
+
: values.map(value => formatAxisValue(value, scale.type, config.format, formatValue))
|
|
84
85
|
};
|
|
85
86
|
}
|
|
86
87
|
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { formatNumericTickValues } from "../../../grammar/ticks.js";
|
|
1
2
|
import { formatVisibleText } from "../../../core/textMetrics.js";
|
|
3
|
+
import { validateBoolean } from "../../../core/validation.js";
|
|
2
4
|
import {
|
|
3
5
|
formatValue,
|
|
4
6
|
isUtcValueFormat,
|
|
@@ -10,6 +12,15 @@ const AXIS_POSITIONS = Object.freeze({
|
|
|
10
12
|
y: Object.freeze(["left", "right"])
|
|
11
13
|
});
|
|
12
14
|
|
|
15
|
+
export function resolveAxisFrameConfig(current, args) {
|
|
16
|
+
if (args.underlay !== undefined) validateBoolean(args.underlay, "Axis underlay");
|
|
17
|
+
const frame = { ...current };
|
|
18
|
+
if (Object.hasOwn(args, "offset")) frame.offset = args.offset;
|
|
19
|
+
if (args.underlay === true) frame.underlay = true;
|
|
20
|
+
else if (args.underlay === false) delete frame.underlay;
|
|
21
|
+
return Object.keys(frame).length === 0 ? undefined : frame;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
export function defaultAxisPosition(channel) {
|
|
14
25
|
return channel === "x" ? "bottom" : "left";
|
|
15
26
|
}
|
|
@@ -96,12 +107,23 @@ export function resolveAxisTitleGeometry({
|
|
|
96
107
|
}
|
|
97
108
|
|
|
98
109
|
export function validateAxisFormat(format) {
|
|
110
|
+
if (format === "ticks") return format;
|
|
99
111
|
return validateValueFormat(format, "Label format", {
|
|
100
112
|
allowDecimalsObject: true
|
|
101
113
|
});
|
|
102
114
|
}
|
|
103
115
|
|
|
116
|
+
export function formatNumericAxisTicks(values, scaleType) {
|
|
117
|
+
if (["ordinal", "band", "point", "time"].includes(scaleType)) {
|
|
118
|
+
throw new Error('Axis format "ticks" requires a quantitative scale.');
|
|
119
|
+
}
|
|
120
|
+
return formatNumericTickValues(values);
|
|
121
|
+
}
|
|
122
|
+
|
|
104
123
|
export function formatAxisValue(value, scaleType, format, autoFormatter) {
|
|
124
|
+
if (format === "ticks") {
|
|
125
|
+
throw new Error('Axis format "ticks" requires the complete settled tick values.');
|
|
126
|
+
}
|
|
105
127
|
validateAxisFormat(format);
|
|
106
128
|
if (format === "auto") return formatVisibleText(autoFormatter(value));
|
|
107
129
|
if (["ordinal", "band", "point"].includes(scaleType)) {
|
|
@@ -8,6 +8,7 @@ const CREATE_OPTIONS = Object.freeze(["target",
|
|
|
8
8
|
"position",
|
|
9
9
|
"count",
|
|
10
10
|
"values",
|
|
11
|
+
"rounding",
|
|
11
12
|
"ticks",
|
|
12
13
|
"labels"
|
|
13
14
|
]);
|
|
@@ -15,6 +16,7 @@ const EDIT_OPTIONS = Object.freeze(["target",
|
|
|
15
16
|
"position",
|
|
16
17
|
"count",
|
|
17
18
|
"values",
|
|
19
|
+
"rounding",
|
|
18
20
|
"ticks",
|
|
19
21
|
"labels"
|
|
20
22
|
]);
|
|
@@ -37,8 +39,8 @@ const LABEL_OPTIONS = Object.freeze([
|
|
|
37
39
|
"colorMap",
|
|
38
40
|
"align"
|
|
39
41
|
]);
|
|
40
|
-
const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values"]);
|
|
41
|
-
const SHARED_EDIT = Object.freeze(["target", "position", "count", "values"]);
|
|
42
|
+
const SHARED_CREATE = Object.freeze(["target", "scale", "position", "count", "values", "rounding"]);
|
|
43
|
+
const SHARED_EDIT = Object.freeze(["target", "position", "count", "values", "rounding"]);
|
|
42
44
|
|
|
43
45
|
function validateNested(value, supported, label) {
|
|
44
46
|
if (!isPlainObject(value)) {
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
valuesFromTickConfig
|
|
28
28
|
} from "../tickValues.js";
|
|
29
29
|
import { DEFAULT_COLORS } from "../../../theme/defaults.js";
|
|
30
|
+
import { validateTickRounding } from "../../../grammar/ticks.js";
|
|
30
31
|
import {
|
|
31
32
|
defaultAxisPosition,
|
|
32
33
|
resolveAxisTickGeometry,
|
|
@@ -35,7 +36,7 @@ import {
|
|
|
35
36
|
import { findCanvasGraphic, resolvePlotGraphicPlacement } from
|
|
36
37
|
"../../../materialization/graphicHierarchy.js";
|
|
37
38
|
|
|
38
|
-
const OPTIONS = Object.freeze(["target", "scale", "position", "count", "values", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
|
|
39
|
+
const OPTIONS = Object.freeze(["target", "scale", "position", "count", "values", "rounding", "length", "color", "lineWidth", "alongOffset", "strokeDash", "strokeDashMap"]);
|
|
39
40
|
const DEFAULTS = Object.freeze({
|
|
40
41
|
count: DEFAULT_TICK_COUNT,
|
|
41
42
|
length: 6,
|
|
@@ -44,6 +45,10 @@ const DEFAULTS = Object.freeze({
|
|
|
44
45
|
});
|
|
45
46
|
|
|
46
47
|
function validateOptions(args, operation, create) {
|
|
48
|
+
validateTickRounding(args.rounding, !Object.hasOwn(args, "rounding"));
|
|
49
|
+
if (Object.hasOwn(args, "rounding") && Object.hasOwn(args, "values")) {
|
|
50
|
+
throw new Error(`${operation} cannot use rounding and values together.`);
|
|
51
|
+
}
|
|
47
52
|
validateOptionObject(
|
|
48
53
|
args,
|
|
49
54
|
create ? OPTIONS : OPTIONS.filter(key => key !== "scale"),
|
|
@@ -165,7 +170,10 @@ function makeEdit(channel) {
|
|
|
165
170
|
...(explicitMode ? { inferredValues: false } : {}),
|
|
166
171
|
mode
|
|
167
172
|
}, args);
|
|
168
|
-
if (mode === "values")
|
|
173
|
+
if (mode === "values") {
|
|
174
|
+
delete config.count;
|
|
175
|
+
if (!Object.hasOwn(args, "rounding")) delete config.rounding;
|
|
176
|
+
} else delete config.values;
|
|
169
177
|
validateConfig(channel, config);
|
|
170
178
|
const resolved = geometry(this, channel, config, axisId);
|
|
171
179
|
let next = this._withGuideConfig(axisId, config).editGraphics({ target: id, property: "length", value: resolved.values.length });
|
|
@@ -179,7 +187,11 @@ function makeEdit(channel) {
|
|
|
179
187
|
next = next.editGraphics({ target: id, property: "strokeDash", value: resolveGuideDashes(resolved.values, config) });
|
|
180
188
|
}
|
|
181
189
|
// Structural dependency plans own grids during a scale/role handoff.
|
|
182
|
-
|
|
190
|
+
const explicitSelection = explicitMode || Object.hasOwn(args, "rounding");
|
|
191
|
+
return explicitSelection ? applyMaterializationPlan(next,
|
|
192
|
+
planAxisTickGridRematerialization(next, channel, axisId, {
|
|
193
|
+
syncLabels: Object.hasOwn(config, "rounding") || Object.hasOwn(previous, "rounding")
|
|
194
|
+
})) : next;
|
|
183
195
|
}));
|
|
184
196
|
}
|
|
185
197
|
|
|
@@ -198,6 +210,10 @@ function makeCreate(channel) {
|
|
|
198
210
|
const id = axisGraphicIds(channel, axisId)[1];
|
|
199
211
|
if (this.graphicSpec.objects[id]) throw new Error(`${op} requires missing axis ticks.`);
|
|
200
212
|
const resolvedScale = this.resolvedScales[scale];
|
|
213
|
+
const labels = this.guideConfigs.axis?.[axisId]?.labels;
|
|
214
|
+
const inheritedSelection = labels?.rounding !== undefined && !Object.hasOwn(args, "values")
|
|
215
|
+
? { count: labels.count, rounding: labels.rounding } : {};
|
|
216
|
+
args = { ...inheritedSelection, ...args };
|
|
201
217
|
const inferredValues =
|
|
202
218
|
Object.hasOwn(args, "count") || Object.hasOwn(args, "values")
|
|
203
219
|
? undefined
|
|
@@ -217,7 +233,9 @@ function makeCreate(channel) {
|
|
|
217
233
|
...resolvePlotGraphicPlacement(this)
|
|
218
234
|
})
|
|
219
235
|
._withGuideConfig(axisId, config)[edit](axisId === channel ? undefined : axisTargetArgs(axisId, channel));
|
|
220
|
-
return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId
|
|
236
|
+
return applyMaterializationPlan(next, planAxisTickGridRematerialization(next, channel, axisId, {
|
|
237
|
+
syncLabels: config.rounding !== undefined || labels?.rounding !== undefined
|
|
238
|
+
}));
|
|
221
239
|
}));
|
|
222
240
|
}
|
|
223
241
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { selectCartesianAxis, axisTargetArgs } from "../../../selectors/axes.js";
|
|
2
2
|
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
3
|
import { materializeTextLines } from "../../../materialization/textLines.js";
|
|
4
|
-
import { axisTitleTextLines as titleLines, validateAxisTextPlacement } from "../../../grammar/axisTextPlacement.js";
|
|
4
|
+
import { axisTitleTextLines as titleLines, validateAxisTextPlacement, axisTitleAllowsLabelOverlap } from "../../../grammar/axisTextPlacement.js";
|
|
5
5
|
import { axisThemeTypography } from "../../theme/state.js";
|
|
6
6
|
import { canvasOverflowError } from "../../../layout/canvas.js";
|
|
7
7
|
import { isSourceOwnedText } from "../../../grammar/text.js";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
import { AXIS_TITLE_FONT_SIZE, DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from
|
|
22
22
|
"../../../theme/defaults.js";
|
|
23
23
|
import { findDataset } from "../../../selectors/datasets.js";
|
|
24
|
+
import { findUpstreamTransform } from "../../../materialization/dataProvenance.js";
|
|
24
25
|
import { formatAggregateTitle } from "../../../grammar/aggregate.js";
|
|
25
26
|
import { finiteMidpoint } from "../../../grammar/numeric.js";
|
|
26
27
|
import {
|
|
@@ -132,7 +133,7 @@ function inferredTitleOffset(program, channel, config, axisId = channel) {
|
|
|
132
133
|
|
|
133
134
|
const CREATE_OPTIONS = ["target",
|
|
134
135
|
"text", "scale", "position", "at", "offset", "rotation", "color",
|
|
135
|
-
"fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline"
|
|
136
|
+
"fontSize", "fontFamily", "fontWeight", "alongOffset", "align", "baseline", "overlap"
|
|
136
137
|
];
|
|
137
138
|
const EDIT_OPTIONS = CREATE_OPTIONS.filter(key => key !== "scale");
|
|
138
139
|
const DEFAULTS = {
|
|
@@ -159,6 +160,8 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
159
160
|
const primaryTitles = new Set();
|
|
160
161
|
for (const layer of program.semanticSpec.layers) {
|
|
161
162
|
if (isSourceOwnedText(layer)) continue;
|
|
163
|
+
// Reference statistics reuse the source axis title.
|
|
164
|
+
if (program.markConfigs[layer.id]?.statisticalReference !== undefined) continue;
|
|
162
165
|
const primary = layer.encoding?.[channel];
|
|
163
166
|
const encoding = layer.mark?.type === "area" && Object.hasOwn(primary ?? {}, "datum")
|
|
164
167
|
? layer.encoding?.[`${channel}2`] : primary;
|
|
@@ -173,9 +176,8 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
173
176
|
encoding.field.length
|
|
174
177
|
) {
|
|
175
178
|
const dataset = findDataset(program, layer.data);
|
|
176
|
-
const transform = dataset
|
|
177
|
-
|
|
178
|
-
: undefined;
|
|
179
|
+
const transform = findUpstreamTransform(program, dataset,
|
|
180
|
+
["density", "ecdf", "interval", "boxSummary", "gradientProfile", "summary"]);
|
|
179
181
|
let derivedTitle;
|
|
180
182
|
if (transform?.type === "density") {
|
|
181
183
|
derivedTitle = encoding.field === transform.as?.[0]
|
|
@@ -183,12 +185,23 @@ export function inferAxisTitleText(program, channel, scaleId) {
|
|
|
183
185
|
: encoding.field === transform.as?.[1]
|
|
184
186
|
? "Density"
|
|
185
187
|
: undefined;
|
|
188
|
+
} else if (transform?.type === "ecdf") {
|
|
189
|
+
derivedTitle = encoding.field === transform.as.value ? transform.field
|
|
190
|
+
: encoding.field === transform.as.probability ? "Cumulative probability"
|
|
191
|
+
: encoding.field === transform.as.cumulative ? "Cumulative count" : undefined;
|
|
186
192
|
} else if (transform?.type === "interval" &&
|
|
187
193
|
Object.values(transform.as).includes(encoding.field)) {
|
|
188
194
|
derivedTitle = `${transform.center}(${transform.field})`;
|
|
189
195
|
} else if (transform?.type === "boxSummary" &&
|
|
190
196
|
Object.values(transform.as).includes(encoding.field)) {
|
|
191
197
|
derivedTitle = transform.field;
|
|
198
|
+
} else if (transform?.type === "gradientProfile" &&
|
|
199
|
+
[transform.as.lower, transform.as.upper, transform.as.center].includes(encoding.field)) {
|
|
200
|
+
// Profile bounds and the optional center share one source measure.
|
|
201
|
+
derivedTitle = transform.field;
|
|
202
|
+
} else if (transform?.type === "summary") {
|
|
203
|
+
const aggregate = transform.aggregates.find(value => value.as === encoding.field);
|
|
204
|
+
if (aggregate) derivedTitle = formatAggregateTitle(aggregate.op, aggregate.field);
|
|
192
205
|
}
|
|
193
206
|
const title = encoding.title ?? derivedTitle ?? (encoding.aggregate === undefined
|
|
194
207
|
? encoding.field
|
|
@@ -228,7 +241,7 @@ function resolveGeometry(program, channel, config, axisId = channel) {
|
|
|
228
241
|
throw canvasOverflowError(`The ${channel}-axis title does not fit the Canvas margin.`, [resolvedBounds], canvas);
|
|
229
242
|
}
|
|
230
243
|
const labelBounds = axisLabelBounds(program, channel, axisId);
|
|
231
|
-
if (labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
|
|
244
|
+
if (!axisTitleAllowsLabelOverlap(config) && labelBounds.some(bounds => bounds && textBoundsIntersect(resolvedBounds, bounds))) {
|
|
232
245
|
throw new Error(`The ${channel}-axis title overlaps the axis labels.`);
|
|
233
246
|
}
|
|
234
247
|
return geometry;
|
|
@@ -91,6 +91,9 @@ function planAxis(program, channel, args, explicit) {
|
|
|
91
91
|
if (explicit.offset !== undefined && explicit.offset !== (configs.frame?.offset ?? 0)) {
|
|
92
92
|
guideConflict(`${channel} axis uses a different frame offset`);
|
|
93
93
|
}
|
|
94
|
+
if (explicit.underlay !== undefined && explicit.underlay !== (configs.frame?.underlay === true)) {
|
|
95
|
+
guideConflict(`${channel} axis uses a different underlay policy`);
|
|
96
|
+
}
|
|
94
97
|
const positions = [...new Set(Object.values(configs).map(config => config.position).filter(Boolean))];
|
|
95
98
|
if (positions.length > 1) guideConflict(`${channel} axis components have incompatible positions`);
|
|
96
99
|
const position = explicit.position ?? positions[0];
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "./resolve.js";
|
|
16
16
|
import { polarGuideNames } from "../polar/resolve.js";
|
|
17
17
|
import { resolveAutomaticGridOptions } from "../applicability.js";
|
|
18
|
+
import { hasPlotUnderlays } from "../../../materialization/plotDrawOrder.js";
|
|
18
19
|
|
|
19
20
|
const CARTESIAN_OPTIONS = Object.freeze(["horizontal", "vertical"]);
|
|
20
21
|
const POLAR_OPTIONS = Object.freeze(["theta", "radial"]);
|
|
@@ -78,7 +79,7 @@ function makeRematerialize(direction) {
|
|
|
78
79
|
value: geometry[property]
|
|
79
80
|
});
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
+
next = editGraphicProperties(next, operation.graphic, {
|
|
82
83
|
stroke: config.color,
|
|
83
84
|
strokeWidth: config.lineWidth
|
|
84
85
|
})
|
|
@@ -87,6 +88,7 @@ function makeRematerialize(direction) {
|
|
|
87
88
|
property: "strokeDash",
|
|
88
89
|
value: geometry.strokeDash
|
|
89
90
|
});
|
|
91
|
+
return hasPlotUnderlays(next) ? next.rematerializePlotDrawOrder() : next;
|
|
90
92
|
}
|
|
91
93
|
);
|
|
92
94
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { cartesianAxisOwners } from "../../../selectors/axes.js";
|
|
2
|
+
import { gridNames } from "../../../selectors/grids.js";
|
|
3
|
+
export { gridNames };
|
|
2
4
|
import { axisGraphicIds } from "../../../materialization/guides/resources.js";
|
|
3
5
|
import { isNominalValue } from "../../../grammar/scales/fields.js";
|
|
4
6
|
import { patchGuideDash, validateGuideDash, normalizeGuideDashMap, resolveGuideDashes } from "../../../grammar/guideDash.js";
|
|
@@ -41,16 +43,6 @@ const DEFAULT_STYLE = Object.freeze({
|
|
|
41
43
|
strokeDash: Object.freeze([])
|
|
42
44
|
});
|
|
43
45
|
|
|
44
|
-
export function gridNames(direction) {
|
|
45
|
-
const prefix = direction === "horizontal" ? "Horizontal" : "Vertical";
|
|
46
|
-
return {
|
|
47
|
-
channel: direction === "horizontal" ? "y" : "x",
|
|
48
|
-
create: `create${prefix}Grid`,
|
|
49
|
-
rematerialize: `rematerialize${prefix}Grid`,
|
|
50
|
-
graphic: `${direction}GridLines`
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
46
|
function validateStrokeDash(value) {
|
|
55
47
|
return validateGuideDash(value, "Grid strokeDash");
|
|
56
48
|
}
|
|
@@ -215,10 +207,12 @@ export function resolveGridConfig(program, direction, args, resources) {
|
|
|
215
207
|
export function refreshGridConfig(program, config) {
|
|
216
208
|
if (config.inferredValues !== true) return config;
|
|
217
209
|
const channel = config.direction === "horizontal" ? "y" : "x";
|
|
210
|
+
const inferred = inferGridTickConfig(program, channel, config.scale);
|
|
218
211
|
const refreshed = {
|
|
219
212
|
...config,
|
|
220
|
-
...
|
|
213
|
+
...inferred
|
|
221
214
|
};
|
|
215
|
+
if (inferred.rounding === undefined) delete refreshed.rounding;
|
|
222
216
|
if (refreshed.mode === "values") delete refreshed.count;
|
|
223
217
|
else delete refreshed.values;
|
|
224
218
|
return refreshed;
|
|
@@ -232,11 +226,13 @@ export function editGridConfig(previous, args) {
|
|
|
232
226
|
)
|
|
233
227
|
};
|
|
234
228
|
if (Object.hasOwn(args, "count")) {
|
|
229
|
+
delete config.rounding;
|
|
235
230
|
config.mode = "count";
|
|
236
231
|
config.count = args.count;
|
|
237
232
|
config.inferredValues = false;
|
|
238
233
|
delete config.values;
|
|
239
234
|
} else if (Array.isArray(args.values)) {
|
|
235
|
+
delete config.rounding;
|
|
240
236
|
config.mode = "values";
|
|
241
237
|
config.values = args.values;
|
|
242
238
|
config.inferredValues = false;
|
|
@@ -37,6 +37,11 @@ function inferAxesOptions(program, applicability, layers) {
|
|
|
37
37
|
? { x: { ticksAndLabels: { count: 7 } } }
|
|
38
38
|
: {};
|
|
39
39
|
const directions = applicability.axes.directions;
|
|
40
|
+
for (const channel of ["x", "y"]) {
|
|
41
|
+
if (!directions[channel] && layers.some(layer => layer.encoding?.[channel] !== undefined)) {
|
|
42
|
+
inferred[channel] = false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
40
45
|
const hasStoredY = layers.some(
|
|
41
46
|
layer => layer.encoding?.y !== undefined
|
|
42
47
|
);
|
|
@@ -126,11 +131,26 @@ export function resolveGuideOptions(program, args = {}, layers = program.semanti
|
|
|
126
131
|
const createGuides = /* @__PURE__ */ action(
|
|
127
132
|
{
|
|
128
133
|
op: "createGuides",
|
|
129
|
-
description: "Create applicable axes, grid, and legend."
|
|
134
|
+
description: "Create applicable axes, grid, and legend.",
|
|
135
|
+
scope: "any"
|
|
130
136
|
},
|
|
131
137
|
function (args = {}) {
|
|
138
|
+
if (this.compositionSpec?.facet) {
|
|
139
|
+
validateOptions(args);
|
|
140
|
+
if (Object.keys(args).length === 0) {
|
|
141
|
+
const selected = Object.entries(resolveGuideOptions(this, args))
|
|
142
|
+
.filter(([, options]) => options !== undefined);
|
|
143
|
+
if (selected.length > 0 && selected.every(([kind]) =>
|
|
144
|
+
hasStoredGuideCollection(this, kind === "axes" ? "axis" : kind)
|
|
145
|
+
)) return this;
|
|
146
|
+
}
|
|
147
|
+
return this.editFacetSource({ action: { method: "createGuides", options: args } });
|
|
148
|
+
}
|
|
149
|
+
this._assertUnitProgram("createGuides");
|
|
132
150
|
const { axes, grid, legend } = resolveGuideOptions(this, args);
|
|
133
151
|
if (axes === undefined && grid === undefined && legend === undefined) {
|
|
152
|
+
if (this.semanticSpec.layers.length > 0 &&
|
|
153
|
+
OPTIONS.some(key => args[key] === undefined)) return this;
|
|
134
154
|
throw new Error("createGuides requires at least one selected guide.");
|
|
135
155
|
}
|
|
136
156
|
|