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
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import { isPlainObject } from "../core/immutable.js";
|
|
2
2
|
import { validateBoxTransform } from "./boxPlot.js";
|
|
3
|
+
import { validateBinTransform } from "./bin.js";
|
|
3
4
|
import {
|
|
4
5
|
requestedBin2DTransform,
|
|
5
6
|
validateBin2DTransform
|
|
6
7
|
} from "./bin2d.js";
|
|
7
8
|
import { validateDensityTransform } from "./density.js";
|
|
9
|
+
import { validateECDFTransform } from "./ecdf.js";
|
|
10
|
+
import { validateComputedTransform } from "./computed.js";
|
|
8
11
|
import { validateFilterTransform } from "./filter.js";
|
|
12
|
+
import { validateFoldTransform } from "./fold.js";
|
|
9
13
|
import {
|
|
10
14
|
requestedGradientProfileTransform,
|
|
11
15
|
validateGradientProfileTransform
|
|
@@ -19,6 +23,8 @@ import { validateMarkFilterTransform } from "./markFilter.js";
|
|
|
19
23
|
import { validateRegressionTransform } from "./regression/index.js";
|
|
20
24
|
import { validateWindowTransform } from "./window.js";
|
|
21
25
|
import { validateTimeUnitTransform } from "./timeUnit.js";
|
|
26
|
+
import { validateSummaryTransform } from "./summary.js";
|
|
27
|
+
import { validateStackTransform } from "./stack.js";
|
|
22
28
|
import { findTransformTopology } from "./transformTopology.js";
|
|
23
29
|
|
|
24
30
|
function requestedDensityTransform(transform) {
|
|
@@ -44,12 +50,22 @@ function facetHorizonTransform(transform, { scales = {} } = {}) {
|
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
const TRANSFORM_POLICIES = Object.freeze({
|
|
53
|
+
bin: Object.freeze({
|
|
54
|
+
...findTransformTopology("bin"),
|
|
55
|
+
validate: validateBinTransform,
|
|
56
|
+
materializeOp: "materializeBinData"
|
|
57
|
+
}),
|
|
47
58
|
bin2d: Object.freeze({
|
|
48
59
|
...findTransformTopology("bin2d"),
|
|
49
60
|
validate: validateBin2DTransform,
|
|
50
61
|
materializeOp: "materializeBin2DData",
|
|
51
62
|
replayTransform: requestedBin2DTransform
|
|
52
63
|
}),
|
|
64
|
+
computed: Object.freeze({
|
|
65
|
+
...findTransformTopology("computed"),
|
|
66
|
+
validate: validateComputedTransform,
|
|
67
|
+
materializeOp: "materializeComputedData"
|
|
68
|
+
}),
|
|
53
69
|
boxOutlier: Object.freeze({
|
|
54
70
|
...findTransformTopology("boxOutlier"),
|
|
55
71
|
validate: validateBoxTransform,
|
|
@@ -66,11 +82,21 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
66
82
|
materializeOp: "materializeDensityData",
|
|
67
83
|
replayTransform: requestedDensityTransform
|
|
68
84
|
}),
|
|
85
|
+
ecdf: Object.freeze({
|
|
86
|
+
...findTransformTopology("ecdf"),
|
|
87
|
+
validate: validateECDFTransform,
|
|
88
|
+
materializeOp: "materializeECDFData"
|
|
89
|
+
}),
|
|
69
90
|
filter: Object.freeze({
|
|
70
91
|
...findTransformTopology("filter"),
|
|
71
92
|
validate: validateFilterTransform,
|
|
72
93
|
materializeOp: "materializeFilteredData"
|
|
73
94
|
}),
|
|
95
|
+
fold: Object.freeze({
|
|
96
|
+
...findTransformTopology("fold"),
|
|
97
|
+
validate: validateFoldTransform,
|
|
98
|
+
materializeOp: "materializeFoldData"
|
|
99
|
+
}),
|
|
74
100
|
gradientProfile: Object.freeze({
|
|
75
101
|
...findTransformTopology("gradientProfile"),
|
|
76
102
|
validate: validateGradientProfileTransform,
|
|
@@ -99,6 +125,16 @@ const TRANSFORM_POLICIES = Object.freeze({
|
|
|
99
125
|
validate: validateRegressionTransform,
|
|
100
126
|
materializeOp: "materializeRegressionData"
|
|
101
127
|
}),
|
|
128
|
+
summary: Object.freeze({
|
|
129
|
+
...findTransformTopology("summary"),
|
|
130
|
+
validate: validateSummaryTransform,
|
|
131
|
+
materializeOp: "materializeSummaryData"
|
|
132
|
+
}),
|
|
133
|
+
stack: Object.freeze({
|
|
134
|
+
...findTransformTopology("stack"),
|
|
135
|
+
validate: validateStackTransform,
|
|
136
|
+
materializeOp: "materializeStackData"
|
|
137
|
+
}),
|
|
102
138
|
timeUnit: Object.freeze({
|
|
103
139
|
...findTransformTopology("timeUnit"),
|
|
104
140
|
validate: validateTimeUnitTransform,
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const NUMERIC_FORMAT = /^\.(0?\d|1[0-2])(f|%|e)$/;
|
|
2
|
+
const TIME_DIRECTIVES = new Set(["Y", "m", "d", "b", "%"]);
|
|
3
|
+
const MONTH_NAMES = Object.freeze([
|
|
4
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
5
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function isUtcValueFormat(format) {
|
|
9
|
+
if (typeof format !== "string") return false;
|
|
10
|
+
let directives = 0;
|
|
11
|
+
for (let index = 0; index < format.length; index += 1) {
|
|
12
|
+
if (format[index] !== "%") continue;
|
|
13
|
+
const directive = format[index + 1];
|
|
14
|
+
if (!TIME_DIRECTIVES.has(directive)) return false;
|
|
15
|
+
if (directive !== "%") directives += 1;
|
|
16
|
+
index += 1;
|
|
17
|
+
}
|
|
18
|
+
return directives > 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function validateValueFormat(format, label = "Label format", {
|
|
22
|
+
allowDecimalsObject = false
|
|
23
|
+
} = {}) {
|
|
24
|
+
if (format === undefined || format === "auto") return format ?? "auto";
|
|
25
|
+
if (
|
|
26
|
+
format !== null &&
|
|
27
|
+
typeof format === "object" &&
|
|
28
|
+
Number.isInteger(format.decimals) &&
|
|
29
|
+
format.decimals >= 0 &&
|
|
30
|
+
format.decimals <= 100 &&
|
|
31
|
+
Object.keys(format).length === 1
|
|
32
|
+
) {
|
|
33
|
+
if (allowDecimalsObject) return format;
|
|
34
|
+
}
|
|
35
|
+
if (typeof format === "string" && (
|
|
36
|
+
NUMERIC_FORMAT.test(format) || isUtcValueFormat(format)
|
|
37
|
+
)) {
|
|
38
|
+
return format;
|
|
39
|
+
}
|
|
40
|
+
const numericCandidate = typeof format === "string"
|
|
41
|
+
? format.match(/^\.(\d+)(f|%|e)$/)
|
|
42
|
+
: undefined;
|
|
43
|
+
if (numericCandidate && Number(numericCandidate[1]) > 12) {
|
|
44
|
+
throw new RangeError(`${label} numeric format supports at most 12 decimals.`);
|
|
45
|
+
}
|
|
46
|
+
throw new TypeError(
|
|
47
|
+
`${label} must be auto${allowDecimalsObject ? ", { decimals }," : ","} or a supported format string (numeric or UTC).`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pad(value, length = 2) {
|
|
52
|
+
return String(value).padStart(length, "0");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatUtcValue(value, format, label) {
|
|
56
|
+
const date = new Date(value);
|
|
57
|
+
if (!Number.isFinite(date.getTime())) {
|
|
58
|
+
throw new TypeError(`${label} requires a valid date or timestamp.`);
|
|
59
|
+
}
|
|
60
|
+
const values = {
|
|
61
|
+
Y: pad(date.getUTCFullYear(), 4),
|
|
62
|
+
m: pad(date.getUTCMonth() + 1),
|
|
63
|
+
d: pad(date.getUTCDate()),
|
|
64
|
+
b: MONTH_NAMES[date.getUTCMonth()]
|
|
65
|
+
};
|
|
66
|
+
return format.replace(/%([Ymdb%])/g, (_, directive) =>
|
|
67
|
+
directive === "%" ? "%" : values[directive]
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatValue(value, {
|
|
72
|
+
format = "auto",
|
|
73
|
+
valueType,
|
|
74
|
+
autoFormatter = current => String(current),
|
|
75
|
+
label = "Label format",
|
|
76
|
+
allowDecimalsObject = false
|
|
77
|
+
} = {}) {
|
|
78
|
+
const resolved = validateValueFormat(format, label, {
|
|
79
|
+
allowDecimalsObject
|
|
80
|
+
});
|
|
81
|
+
if (value === undefined || value === null) return undefined;
|
|
82
|
+
if (resolved === "auto") return autoFormatter(value);
|
|
83
|
+
if (["nominal", "ordinal", "discrete"].includes(valueType)) {
|
|
84
|
+
throw new Error('Discrete labels require format "auto".');
|
|
85
|
+
}
|
|
86
|
+
const utc = isUtcValueFormat(resolved);
|
|
87
|
+
if (valueType === "temporal") {
|
|
88
|
+
if (!utc) throw new Error("Temporal labels require a supported UTC format string.");
|
|
89
|
+
return formatUtcValue(value, resolved, label);
|
|
90
|
+
}
|
|
91
|
+
if (utc) {
|
|
92
|
+
if (valueType === "quantitative") {
|
|
93
|
+
throw new Error("Quantitative labels cannot use a UTC format string.");
|
|
94
|
+
}
|
|
95
|
+
return formatUtcValue(value, resolved, label);
|
|
96
|
+
}
|
|
97
|
+
if (!Number.isFinite(value)) {
|
|
98
|
+
throw new TypeError(`${label} "${typeof resolved === "string" ? resolved : "decimals"}" requires a finite number.`);
|
|
99
|
+
}
|
|
100
|
+
if (typeof resolved === "object") return value.toFixed(resolved.decimals);
|
|
101
|
+
const match = resolved.match(NUMERIC_FORMAT);
|
|
102
|
+
const precision = Number(match[1]);
|
|
103
|
+
if (match[2] === "f") return value.toFixed(precision);
|
|
104
|
+
if (match[2] === "%") {
|
|
105
|
+
const percentage = value * 100;
|
|
106
|
+
if (!Number.isFinite(percentage)) {
|
|
107
|
+
throw new RangeError(`${label} percent format overflow.`);
|
|
108
|
+
}
|
|
109
|
+
return `${percentage.toFixed(precision)}%`;
|
|
110
|
+
}
|
|
111
|
+
return value.toExponential(precision);
|
|
112
|
+
}
|
package/src/grammar/window.js
CHANGED
|
@@ -336,10 +336,16 @@ function stableWindowOutput(operation, calculate) {
|
|
|
336
336
|
}
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
function setWindowOutput(entry, field, value) {
|
|
340
|
+
Object.defineProperty(entry.row, field, {
|
|
341
|
+
value, enumerable: true, writable: true, configurable: true
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
339
345
|
function applyOperation(partition, operation, sortBy) {
|
|
340
346
|
if (operation.op === "rowNumber") {
|
|
341
347
|
partition.forEach((entry, index) => {
|
|
342
|
-
entry
|
|
348
|
+
setWindowOutput(entry, operation.as, index + 1);
|
|
343
349
|
});
|
|
344
350
|
return;
|
|
345
351
|
}
|
|
@@ -354,7 +360,7 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
354
360
|
rank = index + 1;
|
|
355
361
|
denseRank += 1;
|
|
356
362
|
}
|
|
357
|
-
entry
|
|
363
|
+
setWindowOutput(entry, operation.as, operation.op === "rank" ? rank : denseRank);
|
|
358
364
|
});
|
|
359
365
|
return;
|
|
360
366
|
}
|
|
@@ -364,7 +370,7 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
364
370
|
stableFinitePrefixSums(values, label)
|
|
365
371
|
);
|
|
366
372
|
partition.forEach((entry, index) => {
|
|
367
|
-
entry
|
|
373
|
+
setWindowOutput(entry, operation.as, totals[index]);
|
|
368
374
|
});
|
|
369
375
|
return;
|
|
370
376
|
}
|
|
@@ -379,18 +385,18 @@ function applyOperation(partition, operation, sortBy) {
|
|
|
379
385
|
index + operation.frame.following
|
|
380
386
|
);
|
|
381
387
|
const values = operationValues(partition, operation, first, last);
|
|
382
|
-
entry
|
|
388
|
+
setWindowOutput(entry, operation.as, stableWindowOutput(operation, label =>
|
|
383
389
|
calculate(values, label)
|
|
384
|
-
);
|
|
390
|
+
));
|
|
385
391
|
});
|
|
386
392
|
return;
|
|
387
393
|
}
|
|
388
394
|
const direction = operation.op === "lag" ? -1 : 1;
|
|
389
395
|
partition.forEach((entry, index) => {
|
|
390
396
|
const peer = partition[index + direction * operation.offset];
|
|
391
|
-
entry
|
|
397
|
+
setWindowOutput(entry, operation.as, peer === undefined
|
|
392
398
|
? operation.default
|
|
393
|
-
: peer.row[operation.field];
|
|
399
|
+
: peer.row[operation.field]);
|
|
394
400
|
});
|
|
395
401
|
}
|
|
396
402
|
|
package/src/layout/facets.js
CHANGED
|
@@ -23,6 +23,29 @@ function resolveColumns(columns, count) {
|
|
|
23
23
|
return Math.min(value, count);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function resolveGridCoordinates(children, columns) {
|
|
27
|
+
const requested = children.some(child => child.row !== undefined || child.column !== undefined);
|
|
28
|
+
if (!requested) return undefined;
|
|
29
|
+
if (children.some(child => !(
|
|
30
|
+
Number.isInteger(child.row) && child.row >= 0 &&
|
|
31
|
+
Number.isInteger(child.column) && child.column >= 0
|
|
32
|
+
))) {
|
|
33
|
+
throw new RangeError("Facet grid children require non-negative integer row and column coordinates.");
|
|
34
|
+
}
|
|
35
|
+
const keys = children.map(child => `${child.row}:${child.column}`);
|
|
36
|
+
if (new Set(keys).size !== keys.length) {
|
|
37
|
+
throw new Error("Facet grid children must occupy unique row and column coordinates.");
|
|
38
|
+
}
|
|
39
|
+
const requiredColumns = Math.max(...children.map(child => child.column)) + 1;
|
|
40
|
+
if (columns !== undefined && columns !== requiredColumns) {
|
|
41
|
+
throw new RangeError("Facet grid columns must match the declared column domain.");
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
columns: requiredColumns,
|
|
45
|
+
rows: Math.max(...children.map(child => child.row)) + 1
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
function alignedOffset(remaining, align) {
|
|
27
50
|
if (align === "start") return 0;
|
|
28
51
|
if (align === "end") return remaining;
|
|
@@ -43,10 +66,12 @@ export function resolveFacetLayout({
|
|
|
43
66
|
sharedLegendPosition = "right"
|
|
44
67
|
} = {}) {
|
|
45
68
|
const values = children?.map(child => child?.value);
|
|
69
|
+
const coordinates = resolveGridCoordinates(children ?? [], columns);
|
|
46
70
|
const resolvedChildren = normalizeCompositionChildren(children?.map(
|
|
47
|
-
({ value: _value, ...child }) => child
|
|
71
|
+
({ value: _value, row: _row, column: _column, ...child }) => child
|
|
48
72
|
));
|
|
49
|
-
const resolvedColumns =
|
|
73
|
+
const resolvedColumns = coordinates?.columns ??
|
|
74
|
+
resolveColumns(columns, resolvedChildren.length);
|
|
50
75
|
const resolvedGap = validateCompositionSpacing(gap, "Facet gap");
|
|
51
76
|
const resolvedAlign = normalizeCompositionAlign(align);
|
|
52
77
|
const resolvedPadding = normalizeCompositionPadding(padding);
|
|
@@ -79,12 +104,17 @@ export function resolveFacetLayout({
|
|
|
79
104
|
const legendLane = sharedLegend
|
|
80
105
|
? legendGap + (sideLegend ? legendWidth : legendHeight)
|
|
81
106
|
: 0;
|
|
82
|
-
const rowCount =
|
|
107
|
+
const rowCount = coordinates?.rows ??
|
|
108
|
+
Math.ceil(resolvedChildren.length / resolvedColumns);
|
|
83
109
|
const columnWidths = Array(resolvedColumns).fill(-Infinity);
|
|
84
110
|
const rowHeights = Array(rowCount).fill(-Infinity);
|
|
85
111
|
resolvedChildren.forEach((child, index) => {
|
|
86
|
-
const column =
|
|
87
|
-
|
|
112
|
+
const column = coordinates === undefined
|
|
113
|
+
? index % resolvedColumns
|
|
114
|
+
: children[index].column;
|
|
115
|
+
const row = coordinates === undefined
|
|
116
|
+
? Math.floor(index / resolvedColumns)
|
|
117
|
+
: children[index].row;
|
|
88
118
|
columnWidths[column] = Math.max(columnWidths[column], child.width);
|
|
89
119
|
rowHeights[row] = Math.max(rowHeights[row], child.height);
|
|
90
120
|
});
|
|
@@ -97,8 +127,12 @@ export function resolveFacetLayout({
|
|
|
97
127
|
.reduce((sum, height) => sum + height, 0) + resolvedGap * row
|
|
98
128
|
);
|
|
99
129
|
const placements = resolvedChildren.map((child, index) => {
|
|
100
|
-
const column =
|
|
101
|
-
|
|
130
|
+
const column = coordinates === undefined
|
|
131
|
+
? index % resolvedColumns
|
|
132
|
+
: children[index].column;
|
|
133
|
+
const row = coordinates === undefined
|
|
134
|
+
? Math.floor(index / resolvedColumns)
|
|
135
|
+
: children[index].row;
|
|
102
136
|
return {
|
|
103
137
|
id: child.id,
|
|
104
138
|
value: values[index],
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function assertGuideCollisionBlocks(blocks) {
|
|
2
|
+
for (let index = 0; index < blocks.length; index += 1) {
|
|
3
|
+
const a = blocks[index];
|
|
4
|
+
for (const b of blocks.slice(index + 1)) {
|
|
5
|
+
if (a.position !== b.position || a.kind === "axis" && b.kind === "axis") continue;
|
|
6
|
+
if (a.bounds.left < b.bounds.right && a.bounds.right > b.bounds.left &&
|
|
7
|
+
a.bounds.top < b.bounds.bottom && a.bounds.bottom > b.bounds.top) {
|
|
8
|
+
throw new Error(`The ${a.position} ${a.id} and ${b.id} overlap; guides require more margin space or different offsets.`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
package/src/layout/legend.js
CHANGED
|
@@ -3,19 +3,11 @@ import {
|
|
|
3
3
|
measureTextWidth
|
|
4
4
|
} from "../core/textMetrics.js";
|
|
5
5
|
|
|
6
|
-
export function measureLegendSymbolHeight(config) {
|
|
7
|
-
return Math.max(...config.symbol.layers.map(layer => {
|
|
8
|
-
if (layer.type === "swatch") return layer.height;
|
|
9
|
-
if (layer.type === "point") return layer.size * 2;
|
|
10
|
-
return layer.lineWidth;
|
|
11
|
-
}));
|
|
12
|
-
}
|
|
13
|
-
|
|
14
6
|
export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
|
|
15
7
|
return measureTextWidth(formatVisibleText(value), style);
|
|
16
8
|
}
|
|
17
9
|
|
|
18
|
-
export function resolveLegendGrid(config, width, count) {
|
|
10
|
+
export function resolveLegendGrid(config, width, count, symbolHeight) {
|
|
19
11
|
const labels = config.domain.map(formatVisibleText);
|
|
20
12
|
const itemWidths = labels.map(
|
|
21
13
|
label => width + config.labels.offset +
|
|
@@ -39,7 +31,7 @@ export function resolveLegendGrid(config, width, count) {
|
|
|
39
31
|
config.itemGap * Math.max(0, actualColumns - 1);
|
|
40
32
|
const rowHeight = Math.max(
|
|
41
33
|
config.labels.fontSize,
|
|
42
|
-
|
|
34
|
+
symbolHeight
|
|
43
35
|
);
|
|
44
36
|
const gridHeight = rowHeight * rowCount +
|
|
45
37
|
config.itemGap * Math.max(0, rowCount - 1);
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { measureTextWidth, resolveTextBounds } from "../core/textMetrics.js";
|
|
2
|
+
import { alignLegendStart, resolveLegendGrid } from "./legend.js";
|
|
3
|
+
|
|
4
|
+
// Measure item content before choosing its edge. Family owners supply the
|
|
5
|
+
// sample dimensions and formatted labels; this module does not inspect scales.
|
|
6
|
+
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
7
|
+
const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
|
|
8
|
+
? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
|
|
9
|
+
const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
|
|
10
|
+
left: -strokes[index] / 2, right: symbol.width + strokes[index] / 2,
|
|
11
|
+
top: -symbol.height / 2 - strokes[index] / 2,
|
|
12
|
+
bottom: symbol.height / 2 + strokes[index] / 2
|
|
13
|
+
});
|
|
14
|
+
// Retain a family's minimum slot while reserving its complete painted extent.
|
|
15
|
+
const sampleLeft = Math.min(0, ...sampleBounds.map(bounds => bounds.left));
|
|
16
|
+
const sampleRight = Math.max(symbol.width, ...sampleBounds.map(bounds => bounds.right));
|
|
17
|
+
const sampleWidth = sampleRight - sampleLeft;
|
|
18
|
+
const sampleHeight = Math.max(symbol.height,
|
|
19
|
+
...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
|
|
20
|
+
const side = ["left", "right"].includes(config.position);
|
|
21
|
+
const titleVisible = config.titleVisible !== false;
|
|
22
|
+
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
|
|
23
|
+
const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
|
|
24
|
+
const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
|
|
25
|
+
text => measureTextWidth(text, config.labels)
|
|
26
|
+
));
|
|
27
|
+
let symbolX;
|
|
28
|
+
let labelX;
|
|
29
|
+
let itemY;
|
|
30
|
+
let title;
|
|
31
|
+
if (config.layout === "legacy-bottom") {
|
|
32
|
+
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
|
|
33
|
+
const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
|
|
34
|
+
const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
|
|
35
|
+
let cursor = x;
|
|
36
|
+
symbolX = widths.map(width => {
|
|
37
|
+
const start = cursor - sampleLeft;
|
|
38
|
+
cursor += width + config.itemGap;
|
|
39
|
+
return start;
|
|
40
|
+
});
|
|
41
|
+
labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
|
|
42
|
+
itemY = labels.map(() => canvas.height - 28);
|
|
43
|
+
title = { x: x + width / 2, y: canvas.height - 52, align: "center" };
|
|
44
|
+
} else if (side) {
|
|
45
|
+
const width = Math.max(itemWidth, titleWidth);
|
|
46
|
+
const x = config.position === "right" ? plot.x + plot.width + config.offset
|
|
47
|
+
: plot.x - config.offset - width;
|
|
48
|
+
const itemHeight = Math.max(sampleHeight, config.labels.fontSize);
|
|
49
|
+
const pitch = Math.max(config.itemGap, itemHeight);
|
|
50
|
+
symbolX = labels.map(() => x - sampleLeft);
|
|
51
|
+
labelX = labels.map(() => x + sampleWidth + config.labels.offset);
|
|
52
|
+
const firstY = Math.max(plot.y + 52, titleVisible
|
|
53
|
+
? plot.y + 20 + titleHeight / 2 + 12 + itemHeight / 2 : plot.y + 52);
|
|
54
|
+
itemY = labels.map((_, index) => firstY + index * pitch);
|
|
55
|
+
title = { x, y: plot.y + 20, align: "left" };
|
|
56
|
+
} else {
|
|
57
|
+
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
|
|
58
|
+
const inline = config.titlePosition === "left" && titleVisible;
|
|
59
|
+
const prefix = inline ? titleWidth + 20 : 0;
|
|
60
|
+
const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
|
|
61
|
+
const top = !inline && titleVisible ? titleHeight + 12 : 0;
|
|
62
|
+
const height = inline ? Math.max(titleHeight, grid.gridHeight) : top + grid.gridHeight;
|
|
63
|
+
const x = alignLegendStart(plot, width, config.align);
|
|
64
|
+
const y = config.position === "top" ? plot.y - config.offset - height
|
|
65
|
+
: plot.y + plot.height + config.offset;
|
|
66
|
+
const gridX = inline ? x + prefix : x + (width - grid.gridWidth) / 2;
|
|
67
|
+
const gridY = inline ? y + (height - grid.gridHeight) / 2 : y + top;
|
|
68
|
+
let cursor = 0;
|
|
69
|
+
const columns = grid.columnWidths.map(width => {
|
|
70
|
+
const start = cursor;
|
|
71
|
+
cursor += width + config.itemGap;
|
|
72
|
+
return start;
|
|
73
|
+
});
|
|
74
|
+
symbolX = grid.cells.map(cell => gridX + columns[cell.column] - sampleLeft);
|
|
75
|
+
labelX = symbolX.map(value => value + sampleRight + config.labels.offset);
|
|
76
|
+
itemY = grid.cells.map(cell => gridY + grid.rowHeight / 2 + cell.row * (grid.rowHeight + config.itemGap));
|
|
77
|
+
title = inline ? { x, y: y + height / 2, align: "left" }
|
|
78
|
+
: { x: x + width / 2, y: y + titleHeight / 2, align: "center" };
|
|
79
|
+
}
|
|
80
|
+
const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
|
|
81
|
+
x, y, text, ...style, textAlign: align, textBaseline: "middle"
|
|
82
|
+
});
|
|
83
|
+
const bounds = [
|
|
84
|
+
...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
|
|
85
|
+
...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
|
|
86
|
+
...symbolX.map((x, index) => {
|
|
87
|
+
const local = sampleBounds[index];
|
|
88
|
+
return { left: x + local.left, right: x + local.right,
|
|
89
|
+
top: itemY[index] + local.top, bottom: itemY[index] + local.bottom };
|
|
90
|
+
})
|
|
91
|
+
];
|
|
92
|
+
if (config.layout === "legacy-bottom") {
|
|
93
|
+
const itemBounds = titleVisible ? bounds.slice(1) : bounds;
|
|
94
|
+
if (titleVisible && itemBounds.some(bound => bound.top < bounds[0].bottom)) {
|
|
95
|
+
throw new Error("Legacy bottom legend requires more space between its fixed title and item rows.");
|
|
96
|
+
}
|
|
97
|
+
if (bounds.some(bound => bound.top <= plot.y + plot.height)) {
|
|
98
|
+
throw new Error("Legacy bottom legend requires more bottom-margin space.");
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { symbolX, labelX, itemY, title, bounds };
|
|
102
|
+
}
|
package/src/layout/legendLane.js
CHANGED
|
@@ -4,6 +4,28 @@ export const SIDE_LEGEND_SYMBOL_CENTER = 16;
|
|
|
4
4
|
export const SIDE_LEGEND_LABEL_START = 44;
|
|
5
5
|
export const HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP = 12;
|
|
6
6
|
|
|
7
|
+
export function isHorizontalEdgeLegend(config) {
|
|
8
|
+
return config !== undefined &&
|
|
9
|
+
["top", "bottom"].includes(config.position) &&
|
|
10
|
+
config.layout !== "legacy-bottom";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveSingleHorizontalLegendPlacement({ plot, canvas, config, bounds }) {
|
|
14
|
+
if (!isHorizontalEdgeLegend(config)) {
|
|
15
|
+
throw new Error("Single legend placement requires a horizontal edge layout.");
|
|
16
|
+
}
|
|
17
|
+
const dx = config.align === "left" ? plot.x - bounds.left
|
|
18
|
+
: config.align === "right" ? plot.x + plot.width - bounds.right
|
|
19
|
+
: plot.x + plot.width / 2 - midpoint(bounds.left, bounds.right);
|
|
20
|
+
const dy = config.position === "top" ? plot.y - config.offset - bounds.bottom
|
|
21
|
+
: plot.y + plot.height + config.offset - bounds.top;
|
|
22
|
+
const occupied = translateBounds(bounds, dx, dy);
|
|
23
|
+
if (occupied.left < 0 || occupied.right > canvas.width || occupied.top < 0 || occupied.bottom > canvas.height) {
|
|
24
|
+
throw new Error(`Legend layout requires more ${config.position}-margin or Canvas space.`);
|
|
25
|
+
}
|
|
26
|
+
return { dx, dy, occupied };
|
|
27
|
+
}
|
|
28
|
+
|
|
7
29
|
function midpoint(start, end) {
|
|
8
30
|
const sum = start + end;
|
|
9
31
|
const result = Number.isFinite(sum) ? sum / 2 : start / 2 + end / 2;
|
|
@@ -25,24 +47,25 @@ function decoration(border) {
|
|
|
25
47
|
: border.padding + border.lineWidth / 2;
|
|
26
48
|
}
|
|
27
49
|
|
|
28
|
-
function horizontalExtent(blocks) {
|
|
50
|
+
function horizontalExtent(blocks, labelStart) {
|
|
29
51
|
let left = 0;
|
|
30
52
|
let right = 0;
|
|
31
53
|
for (const block of blocks) {
|
|
32
|
-
|
|
54
|
+
const inset = block.inset ?? 0;
|
|
55
|
+
if (block.title !== undefined) right = Math.max(right, block.title.width + inset);
|
|
33
56
|
left = Math.min(left,
|
|
34
|
-
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX
|
|
57
|
+
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.left - block.symbol.centerX - inset
|
|
35
58
|
);
|
|
36
59
|
right = Math.max(
|
|
37
60
|
right,
|
|
38
|
-
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX,
|
|
39
|
-
|
|
61
|
+
SIDE_LEGEND_SYMBOL_CENTER + block.symbol.right - block.symbol.centerX + inset,
|
|
62
|
+
labelStart + block.labels.width + inset
|
|
40
63
|
);
|
|
41
64
|
}
|
|
42
65
|
return { left, right };
|
|
43
66
|
}
|
|
44
67
|
|
|
45
|
-
function placeBlock(block, cursor) {
|
|
68
|
+
function placeBlock(block, cursor, labelStart) {
|
|
46
69
|
let dy;
|
|
47
70
|
if (block.title === undefined) {
|
|
48
71
|
dy = cursor - block.bounds.top;
|
|
@@ -65,7 +88,7 @@ function placeBlock(block, cursor) {
|
|
|
65
88
|
? undefined
|
|
66
89
|
: -block.title.x,
|
|
67
90
|
symbolDx: SIDE_LEGEND_SYMBOL_CENTER - block.symbol.centerX,
|
|
68
|
-
labelDx:
|
|
91
|
+
labelDx: labelStart - block.labels.x,
|
|
69
92
|
bounds
|
|
70
93
|
};
|
|
71
94
|
}
|
|
@@ -87,7 +110,10 @@ export function resolveSideLegendLane({
|
|
|
87
110
|
}
|
|
88
111
|
const blocks = groups.flatMap(group => group.blocks);
|
|
89
112
|
if (blocks.length < 2) return undefined;
|
|
90
|
-
const
|
|
113
|
+
const labelStart = Math.max(SIDE_LEGEND_LABEL_START, ...blocks.map(
|
|
114
|
+
block => SIDE_LEGEND_SYMBOL_CENTER + Math.abs(block.labels.x - block.symbol.centerX)
|
|
115
|
+
));
|
|
116
|
+
const laneExtent = horizontalExtent(blocks, labelStart);
|
|
91
117
|
const offset = blocks.reduce(
|
|
92
118
|
(maximum, block) => Math.max(maximum, block.offset),
|
|
93
119
|
-Infinity
|
|
@@ -96,7 +122,7 @@ export function resolveSideLegendLane({
|
|
|
96
122
|
? plot.x + plot.width + offset
|
|
97
123
|
: plot.x - offset - laneExtent.right;
|
|
98
124
|
const symbolCenterX = titleStartX + SIDE_LEGEND_SYMBOL_CENTER;
|
|
99
|
-
const labelStartX = titleStartX +
|
|
125
|
+
const labelStartX = titleStartX + labelStart;
|
|
100
126
|
const first = blocks[0];
|
|
101
127
|
let cursor = first.title === undefined
|
|
102
128
|
? plot.y + 12
|
|
@@ -110,7 +136,7 @@ export function resolveSideLegendLane({
|
|
|
110
136
|
if (occupiedGroups.length > 0) cursor += inset;
|
|
111
137
|
const groupPlacements = [];
|
|
112
138
|
for (const block of group.blocks) {
|
|
113
|
-
const placement = placeBlock(block, cursor);
|
|
139
|
+
const placement = placeBlock(block, cursor, labelStart);
|
|
114
140
|
groupPlacements.push(placement);
|
|
115
141
|
placements.push({
|
|
116
142
|
...placement,
|
|
@@ -123,7 +149,7 @@ export function resolveSideLegendLane({
|
|
|
123
149
|
cursor = placement.bounds.bottom + SIDE_LEGEND_BLOCK_GAP;
|
|
124
150
|
}
|
|
125
151
|
const vertical = unionBounds(groupPlacements.map(item => item.bounds));
|
|
126
|
-
const groupExtent = horizontalExtent(group.blocks);
|
|
152
|
+
const groupExtent = horizontalExtent(group.blocks, labelStart);
|
|
127
153
|
const foreground = {
|
|
128
154
|
left: titleStartX + groupExtent.left,
|
|
129
155
|
right: titleStartX + groupExtent.right,
|
|
@@ -232,6 +258,13 @@ function normalizeHorizontalRow(entries) {
|
|
|
232
258
|
)
|
|
233
259
|
: commonTitleY + titleDescent + HORIZONTAL_LEGEND_TITLE_ELEMENT_GAP;
|
|
234
260
|
return entries.map(({ group, dx }) => {
|
|
261
|
+
if (group.atomic) {
|
|
262
|
+
const contentDy = elementAnchor - group.element.top;
|
|
263
|
+
const foreground = translateBounds(group.content, dx, contentDy);
|
|
264
|
+
return { id: group.id, dx, titleDy: contentDy, contentDy, foreground,
|
|
265
|
+
occupied: expandBounds(foreground, group.inset), padding: group.padding,
|
|
266
|
+
backgroundId: group.backgroundId };
|
|
267
|
+
}
|
|
235
268
|
const contentDy = commonTitleY === undefined
|
|
236
269
|
? elementAnchor - midpoint(
|
|
237
270
|
group.element.top,
|
|
@@ -350,3 +383,59 @@ export function resolveHorizontalLegendLane({
|
|
|
350
383
|
rowCount: packed.length
|
|
351
384
|
};
|
|
352
385
|
}
|
|
386
|
+
|
|
387
|
+
// Compose independently measured content blocks before the outer edge lane
|
|
388
|
+
// treats the whole group as one indivisible item.
|
|
389
|
+
export function resolveHorizontalLegendGroup({ edge, plot, canvas, groups,
|
|
390
|
+
align, offset, border, backgroundId, collisionBounds = [] }) {
|
|
391
|
+
const inset = decoration(border);
|
|
392
|
+
const innerPlot = { ...plot, x: plot.x + inset, width: plot.width - inset * 2 };
|
|
393
|
+
const packed = packHorizontalRows(groups, innerPlot);
|
|
394
|
+
const rows = packed.map(normalizeHorizontalRow);
|
|
395
|
+
let cursor = 0;
|
|
396
|
+
const placements = [];
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
const bounds = unionBounds(row.map(item => item.occupied));
|
|
399
|
+
const dy = edge === "top" ? cursor - bounds.bottom : cursor - bounds.top;
|
|
400
|
+
const translated = row.map(item => translateHorizontalPlacement(item, dy));
|
|
401
|
+
placements.push(...translated);
|
|
402
|
+
const occupied = unionBounds(translated.map(item => item.occupied));
|
|
403
|
+
cursor = edge === "top" ? occupied.top - HORIZONTAL_LEGEND_BLOCK_GAP
|
|
404
|
+
: occupied.bottom + HORIZONTAL_LEGEND_BLOCK_GAP;
|
|
405
|
+
}
|
|
406
|
+
const foreground = unionBounds(placements.map(item => item.occupied));
|
|
407
|
+
const occupied = expandBounds(foreground, inset);
|
|
408
|
+
const width = occupied.right - occupied.left;
|
|
409
|
+
const x = align === "left" ? plot.x : align === "right" ? plot.x + plot.width - width
|
|
410
|
+
: plot.x + (plot.width - width) / 2;
|
|
411
|
+
const dx = x - occupied.left;
|
|
412
|
+
const dy = edge === "top" ? plot.y - offset - occupied.bottom
|
|
413
|
+
: plot.y + plot.height + offset - occupied.top;
|
|
414
|
+
const finalBounds = translateBounds(occupied, dx, dy);
|
|
415
|
+
if (finalBounds.left < 0 || finalBounds.right > canvas.width ||
|
|
416
|
+
finalBounds.top < 0 || finalBounds.bottom > canvas.height) {
|
|
417
|
+
throw new Error(`Combined legend requires more ${edge}-margin or Canvas space.`);
|
|
418
|
+
}
|
|
419
|
+
if (collisionBounds.some(bounds => overlap(finalBounds, bounds))) {
|
|
420
|
+
throw new Error(`Combined ${edge} legend and ${edge === "top" ? "chart titles" : "x-axis guides"} require more margin space.`);
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
placements: placements.map(item => ({ ...item, dx: item.dx + dx,
|
|
424
|
+
titleDy: item.titleDy + dy, contentDy: item.contentDy + dy,
|
|
425
|
+
...(item.backgroundId === undefined ? {} : { background: {
|
|
426
|
+
id: item.backgroundId,
|
|
427
|
+
x: item.foreground.left + dx - item.padding,
|
|
428
|
+
y: item.foreground.top + dy - item.padding,
|
|
429
|
+
width: item.foreground.right - item.foreground.left + item.padding * 2,
|
|
430
|
+
height: item.foreground.bottom - item.foreground.top + item.padding * 2
|
|
431
|
+
} }) })),
|
|
432
|
+
occupied: finalBounds,
|
|
433
|
+
background: backgroundId === undefined ? undefined : {
|
|
434
|
+
id: backgroundId,
|
|
435
|
+
x: foreground.left + dx - border.padding,
|
|
436
|
+
y: foreground.top + dy - border.padding,
|
|
437
|
+
width: foreground.right - foreground.left + border.padding * 2,
|
|
438
|
+
height: foreground.bottom - foreground.top + border.padding * 2
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
}
|