ggaction 0.0.15 → 0.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -0
- package/README.md +22 -2
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-packet.schema.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +33 -6
- package/src/BasicChartProgram.js +3 -1
- package/src/ChartProgram.js +2 -0
- package/src/accessibility.js +157 -0
- package/src/actions/boxPlots/components.js +6 -8
- package/src/actions/boxPlots/create.js +4 -5
- package/src/actions/boxPlots/edit.js +3 -4
- package/src/actions/boxPlots/materialize.js +1 -1
- package/src/actions/canvas/actions.js +3 -3
- package/src/actions/canvas/fitting.js +1 -1
- package/src/actions/categoryOrder/index.js +6 -8
- package/src/actions/charts/area.js +1 -1
- package/src/actions/charts/bar.js +1 -1
- package/src/actions/charts/beeswarm.js +1 -1
- package/src/actions/charts/density.js +1 -1
- package/src/actions/charts/ecdf.js +2 -2
- package/src/actions/charts/endpoints.js +4 -4
- 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/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/parallel.js +1 -1
- package/src/actions/charts/pie.js +1 -1
- package/src/actions/charts/polar.js +2 -2
- package/src/actions/charts/radar.js +1 -1
- package/src/actions/charts/radial.js +2 -2
- package/src/actions/charts/raincloud.js +2 -2
- package/src/actions/charts/rug-strip.js +2 -2
- package/src/actions/charts/scatter.js +1 -1
- package/src/actions/composition/actions.js +20 -24
- package/src/actions/coordinates/actions.js +1 -1
- package/src/actions/coordinates/edit.js +1 -1
- package/src/actions/data/basicBin2d.js +4 -5
- package/src/actions/data/bin.js +2 -2
- package/src/actions/data/bin2d.js +6 -8
- package/src/actions/data/bin2dMaterialize.js +4 -5
- package/src/actions/data/box.js +4 -4
- package/src/actions/data/complete.js +2 -2
- package/src/actions/data/computed.js +2 -2
- package/src/actions/data/create.js +5 -12
- package/src/actions/data/density.js +3 -3
- package/src/actions/data/derived.js +10 -14
- package/src/actions/data/ecdf.js +2 -2
- package/src/actions/data/edit.js +35 -23
- package/src/actions/data/filter.js +14 -20
- package/src/actions/data/fold.js +2 -2
- package/src/actions/data/gradientProfile.js +2 -2
- package/src/actions/data/horizon.js +2 -2
- package/src/actions/data/impute.js +2 -2
- package/src/actions/data/index.js +55 -54
- package/src/actions/data/interval.js +2 -2
- package/src/actions/data/intervalEdit.js +26 -18
- package/src/actions/data/normalize.js +2 -2
- package/src/actions/data/regression.js +5 -6
- package/src/actions/data/revise.js +48 -0
- package/src/actions/data/revisionBindings.js +13 -33
- package/src/actions/data/stack.js +2 -2
- package/src/actions/data/summary.js +2 -2
- package/src/actions/data/timeUnit.js +2 -2
- package/src/actions/data/window.js +2 -2
- package/src/actions/encodings/angle.js +1 -1
- package/src/actions/encodings/appearance.js +4 -4
- package/src/actions/encodings/barWidth.js +1 -1
- package/src/actions/encodings/channels.js +2 -2
- package/src/actions/encodings/color/index.js +1 -1
- package/src/actions/encodings/density.js +2 -2
- package/src/actions/encodings/histogram.js +1 -1
- package/src/actions/encodings/horizon.js +6 -8
- package/src/actions/encodings/offset.js +2 -2
- package/src/actions/encodings/parallel.js +4 -5
- package/src/actions/encodings/pathOrder.js +6 -11
- package/src/actions/encodings/position/index.js +4 -4
- package/src/actions/encodings/ranged.js +5 -5
- package/src/actions/encodings/remove.js +4 -5
- package/src/actions/encodings/ruleAppearance.js +1 -1
- package/src/actions/encodings/seriesLayout.js +1 -1
- package/src/actions/encodings/stroke.js +1 -1
- package/src/actions/encodings/strokeDash.js +2 -2
- package/src/actions/encodings/text.js +1 -1
- package/src/actions/errorBands/create.js +11 -17
- package/src/actions/errorBands/edit.js +13 -131
- package/src/actions/errorBars/create.js +8 -13
- package/src/actions/errorBars/edit.js +10 -121
- package/src/actions/facets/actions.js +30 -16
- package/src/actions/facets/derive.js +8 -2
- package/src/actions/facets/guides.js +4 -5
- package/src/actions/facets/replay.js +4 -5
- package/src/actions/gradientPlots/components.js +5 -6
- package/src/actions/gradientPlots/create.js +4 -5
- package/src/actions/gradientPlots/edit.js +4 -5
- package/src/actions/gradientPlots/materialize.js +2 -2
- package/src/actions/gradientPlots/rebind.js +4 -5
- package/src/actions/guides/axes/axes.js +23 -4
- package/src/actions/guides/axes/axis.js +2 -2
- package/src/actions/guides/axes/edit.js +2 -2
- package/src/actions/guides/axes/labels.js +9 -9
- package/src/actions/guides/axes/lines.js +9 -7
- package/src/actions/guides/axes/parallel/lifecycle.js +13 -13
- package/src/actions/guides/axes/parallel.js +5 -5
- package/src/actions/guides/axes/remove.js +4 -4
- package/src/actions/guides/axes/tickGroups.js +4 -4
- package/src/actions/guides/axes/ticks.js +13 -4
- package/src/actions/guides/axes/titles.js +10 -10
- package/src/actions/guides/grids/grid.js +18 -29
- package/src/actions/guides/guides.js +1 -1
- package/src/actions/guides/legends/blocks.js +1 -1
- package/src/actions/guides/legends/categorical/actions.js +4 -4
- package/src/actions/guides/legends/categorical/components.js +38 -33
- package/src/actions/guides/legends/categorical/layout.js +1 -1
- package/src/actions/guides/legends/categorical/symbols.js +27 -30
- package/src/actions/guides/legends/continuous/common.js +46 -8
- package/src/actions/guides/legends/continuous/gradient.js +89 -155
- package/src/actions/guides/legends/continuous/interval.js +13 -13
- package/src/actions/guides/legends/continuous/opacity.js +30 -79
- package/src/actions/guides/legends/continuous/stroke.js +22 -322
- package/src/actions/guides/legends/edit.js +1 -1
- package/src/actions/guides/legends/focused.js +5 -5
- package/src/actions/guides/legends/lane.js +6 -6
- package/src/actions/guides/legends/remove.js +4 -5
- package/src/actions/guides/legends/size.js +21 -82
- package/src/actions/guides/legends/strokeWidth.js +17 -77
- package/src/actions/guides/polar/axes/facade.js +4 -4
- package/src/actions/guides/polar/axes/labels.js +5 -5
- package/src/actions/guides/polar/axes/lines.js +9 -15
- package/src/actions/guides/polar/axes/ticks.js +9 -15
- package/src/actions/guides/polar/axes/titles.js +5 -5
- package/src/actions/guides/polar/grids.js +11 -13
- package/src/actions/index.js +2 -0
- package/src/actions/intervals/revision.js +126 -0
- package/src/actions/marks/arc/actions.js +3 -3
- package/src/actions/marks/area/actions.js +4 -4
- package/src/actions/marks/bar/create.js +1 -1
- package/src/actions/marks/bar/edit.js +1 -1
- package/src/actions/marks/bar/materialize.js +12 -38
- package/src/actions/marks/line/actions.js +3 -3
- package/src/actions/marks/point/create.js +1 -1
- package/src/actions/marks/point/edit.js +1 -1
- package/src/actions/marks/point/jitter.js +6 -8
- package/src/actions/marks/point/materialize.js +16 -24
- package/src/actions/marks/point/packing.js +6 -8
- package/src/actions/marks/rect/actions.js +3 -3
- package/src/actions/marks/references.js +8 -10
- package/src/actions/marks/remove.js +5 -49
- package/src/actions/marks/rule/actions.js +4 -4
- package/src/actions/marks/text/actions.js +5 -5
- package/src/actions/marks/text/layout.js +12 -20
- package/src/actions/marks/text/removal.js +1 -1
- package/src/actions/marks/text/selection.js +4 -5
- package/src/actions/marks/tick/actions.js +3 -3
- package/src/actions/primitives/createGraphics.js +5 -4
- package/src/actions/primitives/editGraphics.js +5 -4
- package/src/actions/primitives/graphicProperties.js +6 -0
- package/src/actions/primitives/semantic.js +10 -3
- package/src/actions/primitives/semanticAction.js +7 -4
- package/src/actions/primitives/semanticValidation/dataset.js +2 -4
- package/src/actions/regression/components.js +10 -14
- package/src/actions/regression/create.js +3 -4
- package/src/actions/regression/edit.js +3 -4
- package/src/actions/resources/remove.js +12 -11
- package/src/actions/scales/create.js +1 -1
- package/src/actions/scales/edit.js +1 -1
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/offset.js +2 -2
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/quantitativeColor.js +4 -5
- package/src/actions/selection/actions.js +25 -36
- package/src/actions/textMetrics/index.js +56 -0
- package/src/actions/theme/actions.js +2 -2
- package/src/actions/theme/reconcile.js +65 -121
- package/src/actions/titles/actions.js +8 -8
- package/src/actions/titles/resolve.js +3 -3
- package/src/actions/violinPlots/create.js +1 -1
- package/src/actions/violinPlots/edit.js +1 -1
- package/src/core/action.js +107 -35
- package/src/core/diagnostics.js +16 -0
- package/src/core/extensionRegistry.js +27 -0
- package/src/core/font.js +18 -0
- package/src/core/identifiers.js +5 -4
- package/src/core/immutable.js +10 -3
- package/src/core/optionRequirements.js +8 -0
- package/src/core/programState.js +1 -1
- package/src/core/resourceReferences.js +15 -6
- package/src/core/textMetricProfile.js +28 -0
- package/src/core/textMetrics.js +24 -3
- package/src/core/validation.js +22 -10
- package/src/diagnostics.js +1 -0
- package/src/grammar/impute.js +3 -2
- package/src/grammar/scales/types.js +8 -0
- package/src/grammar/schemas/graphicBounds.js +11 -11
- package/src/layout/labels.js +11 -10
- package/src/layout/legend.js +4 -4
- package/src/layout/legendItems.js +6 -6
- package/src/layout/text.js +10 -9
- package/src/layout/title.js +9 -9
- package/src/materialization/facetGuides/placement.js +1 -1
- package/src/materialization/facetGuides/preparation.js +1 -1
- package/src/materialization/facetHeaders.js +11 -10
- package/src/materialization/guides/layout.js +3 -3
- package/src/materialization/scaleGuideDependencies.js +10 -0
- package/src/materialization/selection/items/common.js +1 -1
- package/src/materialization/text.js +2 -2
- package/src/materialization/typography.js +31 -0
- package/src/mcp/cli.js +5 -3
- package/src/persistence/codec.js +65 -0
- package/src/persistence/validation.js +185 -0
- package/src/persistence.js +74 -0
- package/src/renderers/canvas/index.js +5 -4
- package/src/renderers/nativeBackend.js +10 -0
- package/src/renderers/options.js +13 -0
- package/src/renderers/pdf.js +25 -54
- package/src/renderers/png.js +22 -17
- package/src/renderers/svg.js +10 -14
- package/src/renderers/text.js +1 -11
- package/src/selectors/coordinates.js +3 -1
- package/src/selectors/datasets.js +8 -6
- package/src/selectors/layers.js +6 -4
- package/src/selectors/markOwners.js +46 -0
- package/src/selectors/scales.js +6 -4
- package/src/theme/defaults.js +2 -1
- package/src/version.js +2 -0
- package/types/accessibility.d.ts +37 -0
- package/types/basic.d.ts +4 -2
- package/types/diagnostics.d.ts +15 -0
- package/types/index.d.ts +5 -0
- package/types/pdf.d.ts +9 -0
- package/types/persistence.d.ts +7 -0
- package/types/png.d.ts +9 -0
- package/types/program.d.ts +34 -1
|
@@ -4,6 +4,32 @@ import { isPlainObject } from "./immutable.js";
|
|
|
4
4
|
const ACTION_NAME_PATTERN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
5
|
const DEFINITION_KEYS = new Set(["name", "actions"]);
|
|
6
6
|
const registeredExtensionNames = new Set();
|
|
7
|
+
const extensionByAction = new Map();
|
|
8
|
+
const builtinOpsByClass = new WeakMap();
|
|
9
|
+
|
|
10
|
+
export function recordBuiltinActions(ProgramClass, additional = []) {
|
|
11
|
+
builtinOpsByClass.set(ProgramClass, new Set([
|
|
12
|
+
...Object.getOwnPropertyNames(ProgramClass.prototype).flatMap(name => {
|
|
13
|
+
const metadata = getWrappedActionMetadata(ProgramClass.prototype[name]);
|
|
14
|
+
return metadata ? [metadata.op] : [];
|
|
15
|
+
}), ...additional
|
|
16
|
+
]));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isBuiltinProgramClass(ProgramClass) {
|
|
20
|
+
return builtinOpsByClass.has(ProgramClass);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function requiredTraceExtension(ProgramClass, op) {
|
|
24
|
+
if (op === "program" || builtinOpsByClass.get(ProgramClass)?.has(op)) return undefined;
|
|
25
|
+
const extension = extensionByAction.get(op);
|
|
26
|
+
if (extension !== undefined) return extension;
|
|
27
|
+
throw new TypeError(`Cannot persist unregistered action "${op}".`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasRegisteredExtension(name) {
|
|
31
|
+
return registeredExtensionNames.has(name);
|
|
32
|
+
}
|
|
7
33
|
|
|
8
34
|
function readDataProperty(object, key, owner) {
|
|
9
35
|
const descriptor = Object.getOwnPropertyDescriptor(object, key);
|
|
@@ -101,4 +127,5 @@ export function registerProgramExtension(ProgramClass, definition) {
|
|
|
101
127
|
|
|
102
128
|
Object.defineProperties(ProgramClass.prototype, descriptors);
|
|
103
129
|
registeredExtensionNames.add(name);
|
|
130
|
+
for (const actionName of actionNames) extensionByAction.set(actionName, name);
|
|
104
131
|
}
|
package/src/core/font.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const DEFAULT_FONT_FAMILY = "sans-serif";
|
|
2
|
+
|
|
3
|
+
export function normalizeRendererFontWeight(fontWeight) {
|
|
4
|
+
if (typeof fontWeight !== "number") return fontWeight;
|
|
5
|
+
return Math.min(900, Math.max(100, Math.round(fontWeight / 100) * 100));
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function textMetricKey({ text, fontFamily, fontSize, fontWeight }) {
|
|
9
|
+
return JSON.stringify([text, fontFamily, fontSize, fontWeight]);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function textMetricFontWeight(value = "normal") {
|
|
13
|
+
if (typeof value === "number") return normalizeRendererFontWeight(value);
|
|
14
|
+
if (value === "normal") return 400;
|
|
15
|
+
if (value === "bold") return 700;
|
|
16
|
+
if (/^[1-9]00$/u.test(value)) return Number(value);
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
package/src/core/identifiers.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { annotateError } from "./diagnostics.js";
|
|
1
2
|
export const USER_ID_SOURCE = "[A-Za-z0-9_-]+";
|
|
2
3
|
|
|
3
4
|
const USER_ID_PATTERN = new RegExp(`^${USER_ID_SOURCE}$`);
|
|
4
5
|
|
|
5
6
|
export function validateUserId(id, label = "ID") {
|
|
6
7
|
if (typeof id !== "string" || !USER_ID_PATTERN.test(id)) {
|
|
7
|
-
throw new TypeError(
|
|
8
|
+
throw annotateError(new TypeError(
|
|
8
9
|
`${label} must contain only letters, numbers, _ or - and must not be empty.`
|
|
9
|
-
);
|
|
10
|
+
), { code: "invalid-value", optionPath: label });
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
return id;
|
|
@@ -20,9 +21,9 @@ export function resolveOptionalUserId(id, {
|
|
|
20
21
|
}) {
|
|
21
22
|
if (id !== undefined) return validateUserId(id, label);
|
|
22
23
|
if (ambiguous) {
|
|
23
|
-
throw new Error(
|
|
24
|
+
throw annotateError(new Error(
|
|
24
25
|
`${operation} requires an explicit ${label.toLowerCase()} because its default is ambiguous.`
|
|
25
|
-
);
|
|
26
|
+
), { code: "ambiguous-resource", operation, optionPath: label });
|
|
26
27
|
}
|
|
27
28
|
return validateUserId(defaultId, label);
|
|
28
29
|
}
|
package/src/core/immutable.js
CHANGED
|
@@ -22,11 +22,18 @@ export function isOwned(value) {
|
|
|
22
22
|
return value !== null && typeof value === "object" && ownedValues.has(value);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function cloneAndFreeze(value, ancestors = new WeakSet()) {
|
|
25
|
+
export function cloneAndFreeze(value, ancestors = new WeakSet(), path = "state") {
|
|
26
|
+
if (typeof value === "function") {
|
|
27
|
+
throw new TypeError(`Cannot store a function at ${path} in a ChartProgram.`);
|
|
28
|
+
}
|
|
26
29
|
if (value === null || typeof value !== "object") {
|
|
27
30
|
return value;
|
|
28
31
|
}
|
|
29
32
|
|
|
33
|
+
// Only library-owned values are recursively immutable; Object.freeze alone
|
|
34
|
+
// does not prove ownership of a caller's descendants.
|
|
35
|
+
if (isOwned(value)) return value;
|
|
36
|
+
|
|
30
37
|
if (ancestors.has(value)) {
|
|
31
38
|
throw new TypeError("Cannot store cyclic values in a ChartProgram.");
|
|
32
39
|
}
|
|
@@ -36,12 +43,12 @@ export function cloneAndFreeze(value, ancestors = new WeakSet()) {
|
|
|
36
43
|
let clone;
|
|
37
44
|
|
|
38
45
|
if (Array.isArray(value)) {
|
|
39
|
-
clone = value.map(item => cloneAndFreeze(item, ancestors));
|
|
46
|
+
clone = value.map((item, index) => cloneAndFreeze(item, ancestors, `${path}[${index}]`));
|
|
40
47
|
} else if (isPlainObject(value)) {
|
|
41
48
|
clone = Object.fromEntries(
|
|
42
49
|
Object.entries(value).map(([key, item]) => [
|
|
43
50
|
key,
|
|
44
|
-
cloneAndFreeze(item, ancestors)
|
|
51
|
+
cloneAndFreeze(item, ancestors, `${path}.${key}`)
|
|
45
52
|
])
|
|
46
53
|
);
|
|
47
54
|
} else {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// The method's value-producing contract is shared by runtime validation and
|
|
2
|
+
// authoring knowledge; editing can inherit these values from stored state.
|
|
3
|
+
export const IMPUTE_REQUIRED_OPTIONS = Object.freeze({
|
|
4
|
+
constant: Object.freeze(["value"]),
|
|
5
|
+
forward: Object.freeze(["sortBy"]),
|
|
6
|
+
backward: Object.freeze(["sortBy"]),
|
|
7
|
+
linear: Object.freeze(["sortBy"])
|
|
8
|
+
});
|
package/src/core/programState.js
CHANGED
|
@@ -23,7 +23,7 @@ export function ownChildPrograms(children, ProgramClass) {
|
|
|
23
23
|
if (!(program instanceof ProgramClass)) {
|
|
24
24
|
throw new TypeError(`ChartProgram child "${id}" must be a ChartProgram.`);
|
|
25
25
|
}
|
|
26
|
-
owned
|
|
26
|
+
Object.defineProperty(owned, id, { value: program, enumerable: true });
|
|
27
27
|
}
|
|
28
28
|
return freezeOwned(owned);
|
|
29
29
|
}
|
|
@@ -148,14 +148,19 @@ export function collectMarkConfigReferences(program) {
|
|
|
148
148
|
"coordinate", value, "markConfig", ownerId, path
|
|
149
149
|
));
|
|
150
150
|
}
|
|
151
|
-
|
|
151
|
+
const reservedScale = root === "regression" && key === "colorScale" && config.regression.groupBy === undefined;
|
|
152
|
+
if (!reservedScale && (key === "scale" || key.endsWith("Scale")) && typeof value === "string") {
|
|
152
153
|
append(references, reference("scale", value, "markConfig", ownerId, path));
|
|
153
154
|
} else if (key === "scale" && isPlainObject(value)) {
|
|
154
155
|
append(references, reference(
|
|
155
156
|
"scale", value.id, "markConfig", ownerId, [...path, "id"]
|
|
156
157
|
));
|
|
157
158
|
}
|
|
158
|
-
|
|
159
|
+
const inactiveMark = (root === "boxPlot" && key === "outlierId" && !Object.hasOwn(program.markConfigs, value)) ||
|
|
160
|
+
(root === "errorBar" && ["lowerCapId", "upperCapId"].includes(key) && config.errorBar.caps === false) ||
|
|
161
|
+
(root === "errorBand" && ["lowerBoundaryId", "upperBoundaryId"].includes(key) &&
|
|
162
|
+
!Object.hasOwn(program.markConfigs, value));
|
|
163
|
+
if (MARK_ID_KEYS.has(key) && !inactiveMark) {
|
|
159
164
|
append(references, reference("mark", value, "markConfig", ownerId, path));
|
|
160
165
|
}
|
|
161
166
|
if (
|
|
@@ -316,7 +321,13 @@ export function collectResourceReferences(program, { kind, id }) {
|
|
|
316
321
|
throw new Error(`Unknown resource kind "${kind}".`);
|
|
317
322
|
}
|
|
318
323
|
const target = validateUserId(id, "Resource id");
|
|
319
|
-
const combined =
|
|
324
|
+
const combined = collectAllResourceReferences(program).filter(value => value.kind === kind && value.id === target);
|
|
325
|
+
const unique = new Map(combined.map(value => [referenceKey(value), value]));
|
|
326
|
+
return freezeOwned([...unique.values()].sort(compareReferences));
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export function collectAllResourceReferences(program) {
|
|
330
|
+
return [
|
|
320
331
|
...collectSemanticDataReferences(program),
|
|
321
332
|
...collectSemanticScaleReferences(program),
|
|
322
333
|
...collectSemanticCoordinateReferences(program),
|
|
@@ -326,7 +337,5 @@ export function collectResourceReferences(program, { kind, id }) {
|
|
|
326
337
|
...collectDataOwnerReferences(program),
|
|
327
338
|
...collectCompositionReferences(program),
|
|
328
339
|
...collectContextReferences(program)
|
|
329
|
-
]
|
|
330
|
-
const unique = new Map(combined.map(value => [referenceKey(value), value]));
|
|
331
|
-
return freezeOwned([...unique.values()].sort(compareReferences));
|
|
340
|
+
];
|
|
332
341
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { cloneAndFreeze } from "./immutable.js";
|
|
2
|
+
import { validateUserId } from "./identifiers.js";
|
|
3
|
+
import { validateOptionObject, validatePositiveFinite, validateNonNegativeFinite, validateNonEmptyString } from "./validation.js";
|
|
4
|
+
|
|
5
|
+
import { textMetricKey } from "./font.js";
|
|
6
|
+
|
|
7
|
+
export function normalizeTextMetricProfile(profile) {
|
|
8
|
+
validateOptionObject(profile, ["schemaVersion", "id", "measurements"], "Text metrics profile");
|
|
9
|
+
if (profile.schemaVersion !== 1) throw new Error("Text metrics require schemaVersion 1.");
|
|
10
|
+
validateUserId(profile.id, "Text metrics profile id");
|
|
11
|
+
if (!Array.isArray(profile.measurements)) throw new TypeError("Text metrics measurements must be an array.");
|
|
12
|
+
const seen = new Set();
|
|
13
|
+
for (const measurement of profile.measurements) {
|
|
14
|
+
validateOptionObject(measurement, ["text", "fontFamily", "fontSize", "fontWeight", "width"], "Text measurement");
|
|
15
|
+
if (typeof measurement.text !== "string") throw new TypeError("Text measurement text must be a string.");
|
|
16
|
+
validateNonEmptyString(measurement.fontFamily, "Text measurement fontFamily");
|
|
17
|
+
validatePositiveFinite(measurement.fontSize, "Text measurement fontSize");
|
|
18
|
+
validateNonNegativeFinite(measurement.width, "Text measurement width");
|
|
19
|
+
if (!Number.isInteger(measurement.fontWeight) || measurement.fontWeight < 100 ||
|
|
20
|
+
measurement.fontWeight > 900 || measurement.fontWeight % 100 !== 0) {
|
|
21
|
+
throw new RangeError("Text measurement fontWeight must be 100–900 in steps of 100.");
|
|
22
|
+
}
|
|
23
|
+
const key = textMetricKey(measurement);
|
|
24
|
+
if (seen.has(key)) throw new Error("Duplicate text measurement combination.");
|
|
25
|
+
seen.add(key);
|
|
26
|
+
}
|
|
27
|
+
return cloneAndFreeze(profile);
|
|
28
|
+
}
|
package/src/core/textMetrics.js
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import { DEFAULT_FONT_FAMILY, textMetricFontWeight, textMetricKey } from "./font.js";
|
|
2
|
+
|
|
3
|
+
import { isOwned } from "./immutable.js";
|
|
4
|
+
|
|
5
|
+
const measurementIndexes = new WeakMap();
|
|
6
|
+
|
|
7
|
+
function measuredWidth(profile, text, style) {
|
|
8
|
+
if (profile === undefined) return undefined;
|
|
9
|
+
let index = measurementIndexes.get(profile);
|
|
10
|
+
if (index === undefined) {
|
|
11
|
+
index = new Map(profile.measurements.map(measurement =>
|
|
12
|
+
[textMetricKey(measurement), measurement.width]));
|
|
13
|
+
if (isOwned(profile)) measurementIndexes.set(profile, index);
|
|
14
|
+
}
|
|
15
|
+
return index.get(textMetricKey({ text, fontFamily: style.fontFamily ?? DEFAULT_FONT_FAMILY,
|
|
16
|
+
fontSize: style.fontSize, fontWeight: textMetricFontWeight(style.fontWeight) }));
|
|
17
|
+
}
|
|
18
|
+
|
|
1
19
|
function codePointWidth(codePoint) {
|
|
2
20
|
if (/\s/u.test(codePoint)) return 0.28;
|
|
3
21
|
if (/[iIl.,:;!'|]/u.test(codePoint)) return 0.27;
|
|
@@ -25,7 +43,8 @@ export function textBoundsFitCanvas(bounds, canvas) {
|
|
|
25
43
|
|
|
26
44
|
export function measureTextWidth(
|
|
27
45
|
text,
|
|
28
|
-
{ fontSize, fontFamily, fontWeight } = {}
|
|
46
|
+
{ fontSize, fontFamily, fontWeight } = {},
|
|
47
|
+
profile
|
|
29
48
|
) {
|
|
30
49
|
if (typeof text !== "string") {
|
|
31
50
|
throw new TypeError("Text measurement requires a string.");
|
|
@@ -33,6 +52,8 @@ export function measureTextWidth(
|
|
|
33
52
|
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
34
53
|
throw new RangeError("Text measurement requires a positive fontSize.");
|
|
35
54
|
}
|
|
55
|
+
const measured = measuredWidth(profile, text, { fontSize, fontFamily, fontWeight });
|
|
56
|
+
if (measured !== undefined) return measured;
|
|
36
57
|
let joined = false;
|
|
37
58
|
let width = 0;
|
|
38
59
|
for (const codePoint of text) {
|
|
@@ -76,11 +97,11 @@ export function resolveTextBounds({
|
|
|
76
97
|
textAlign = "left",
|
|
77
98
|
textBaseline = "alphabetic",
|
|
78
99
|
rotation = 0
|
|
79
|
-
} = {}) {
|
|
100
|
+
} = {}, profile) {
|
|
80
101
|
if (![x, y, rotation].every(Number.isFinite)) {
|
|
81
102
|
throw new TypeError("Text bounds require finite x, y, and rotation values.");
|
|
82
103
|
}
|
|
83
|
-
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight });
|
|
104
|
+
const width = measureTextWidth(text, { fontSize, fontFamily, fontWeight }, profile);
|
|
84
105
|
const [left, right] = textAlign === "center"
|
|
85
106
|
? [-width / 2, width / 2]
|
|
86
107
|
: ["right", "end"].includes(textAlign) ? [-width, 0] : [0, width];
|
package/src/core/validation.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
|
+
import { annotateError } from "./diagnostics.js";
|
|
1
2
|
import { isPlainObject } from "./immutable.js";
|
|
2
3
|
|
|
3
4
|
export const MAX_GENERATED_ITEMS = 10_000;
|
|
4
5
|
export const MAX_WORK_ITEMS = 10_000_000;
|
|
5
6
|
|
|
7
|
+
export function validateDataRows(values, label = "Dataset") {
|
|
8
|
+
if (!Array.isArray(values)) {
|
|
9
|
+
throw annotateError(new TypeError(`${label} requires values to be an array.`), { code: "invalid-value", optionPath: "values" });
|
|
10
|
+
}
|
|
11
|
+
for (let index = 0; index < values.length; index += 1) {
|
|
12
|
+
if (!Object.hasOwn(values, index) || !isPlainObject(values[index])) {
|
|
13
|
+
throw annotateError(new TypeError(`${label} requires every row to be a plain object; invalid row at index ${index}.`), { code: "invalid-value", optionPath: `values[${index}]` });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
6
18
|
export function validateGeneratedItemLimit(
|
|
7
19
|
value,
|
|
8
20
|
label,
|
|
9
21
|
maximum = MAX_GENERATED_ITEMS
|
|
10
22
|
) {
|
|
11
23
|
if (value > maximum) {
|
|
12
|
-
throw new RangeError(`${label} must not exceed ${maximum}.`);
|
|
24
|
+
throw annotateError(new RangeError(`${label} must not exceed ${maximum}.`), { code: "resource-limit", optionPath: label, limit: maximum, actual: value });
|
|
13
25
|
}
|
|
14
26
|
return value;
|
|
15
27
|
}
|
|
@@ -21,7 +33,7 @@ export function validateWorkLimit(value, label) {
|
|
|
21
33
|
export function validateKeys(value, supported, label) {
|
|
22
34
|
for (const key of Object.keys(value)) {
|
|
23
35
|
if (!supported.includes(key)) {
|
|
24
|
-
throw new Error(`Unknown ${label} option "${key}".`);
|
|
36
|
+
throw annotateError(new Error(`Unknown ${label} option "${key}".`), { code: "invalid-option", optionPath: key });
|
|
25
37
|
}
|
|
26
38
|
}
|
|
27
39
|
}
|
|
@@ -33,18 +45,18 @@ export function validateOptionObject(value, supported, label, {
|
|
|
33
45
|
emptyError = TypeError
|
|
34
46
|
} = {}) {
|
|
35
47
|
if (!isPlainObject(value)) {
|
|
36
|
-
throw new TypeError(plainObjectMessage);
|
|
48
|
+
throw annotateError(new TypeError(plainObjectMessage), { code: "invalid-option" });
|
|
37
49
|
}
|
|
38
50
|
if (supported !== undefined) validateKeys(value, supported, label);
|
|
39
51
|
if (!allowEmpty && Object.keys(value).length === 0) {
|
|
40
|
-
throw new emptyError(emptyMessage);
|
|
52
|
+
throw annotateError(new emptyError(emptyMessage), { code: "invalid-option" });
|
|
41
53
|
}
|
|
42
54
|
return value;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
57
|
export function noOptions(args, operation) {
|
|
46
58
|
if (!isPlainObject(args) || Object.keys(args).length > 0) {
|
|
47
|
-
throw new Error(`${operation} does not accept options.`);
|
|
59
|
+
throw annotateError(new Error(`${operation} does not accept options.`), { code: "invalid-option", operation });
|
|
48
60
|
}
|
|
49
61
|
}
|
|
50
62
|
|
|
@@ -59,30 +71,30 @@ export function sameOrderedValues(left, right) {
|
|
|
59
71
|
|
|
60
72
|
export function validateNonEmptyString(value, label) {
|
|
61
73
|
if (typeof value !== "string" || value.length === 0) {
|
|
62
|
-
throw new TypeError(`${label} must be a non-empty string.`);
|
|
74
|
+
throw annotateError(new TypeError(`${label} must be a non-empty string.`), { code: "invalid-value", optionPath: label });
|
|
63
75
|
}
|
|
64
76
|
return value;
|
|
65
77
|
}
|
|
66
78
|
|
|
67
79
|
export function validateUnitInterval(value, label) {
|
|
68
80
|
if (!Number.isFinite(value) || value < 0 || value > 1) {
|
|
69
|
-
throw new RangeError(
|
|
81
|
+
throw annotateError(new RangeError(
|
|
70
82
|
`${label} must be between 0 and 1 (values from 0 to 1).`
|
|
71
|
-
);
|
|
83
|
+
), { code: "invalid-value", optionPath: label });
|
|
72
84
|
}
|
|
73
85
|
return value;
|
|
74
86
|
}
|
|
75
87
|
|
|
76
88
|
export function validatePositiveFinite(value, label) {
|
|
77
89
|
if (!Number.isFinite(value) || value <= 0) {
|
|
78
|
-
throw new RangeError(`${label} must be a positive finite number.`);
|
|
90
|
+
throw annotateError(new RangeError(`${label} must be a positive finite number.`), { code: "invalid-value", optionPath: label });
|
|
79
91
|
}
|
|
80
92
|
return value;
|
|
81
93
|
}
|
|
82
94
|
|
|
83
95
|
export function validateNonNegativeFinite(value, label) {
|
|
84
96
|
if (!Number.isFinite(value) || value < 0) {
|
|
85
|
-
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
97
|
+
throw annotateError(new RangeError(`${label} must be a non-negative finite number.`), { code: "invalid-value", optionPath: label });
|
|
86
98
|
}
|
|
87
99
|
return value;
|
|
88
100
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getErrorDetails } from "./core/diagnostics.js";
|
package/src/grammar/impute.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { IMPUTE_REQUIRED_OPTIONS } from "../core/optionRequirements.js";
|
|
1
2
|
import { cloneAndFreeze, isPlainObject } from "../core/immutable.js";
|
|
2
3
|
import { interpolateNumber, inverseLerp } from "./numeric.js";
|
|
3
4
|
import { normalizeTemporalValue } from "./scales/fields.js";
|
|
@@ -6,7 +7,7 @@ const TRANSFORM_KEYS = Object.freeze([
|
|
|
6
7
|
"type", "fields", "groupBy", "sortBy", "method", "value", "edges", "maxGap"
|
|
7
8
|
]);
|
|
8
9
|
const SORT_KEYS = Object.freeze(["field", "order"]);
|
|
9
|
-
const METHODS = Object.freeze(
|
|
10
|
+
const METHODS = Object.freeze(Object.keys(IMPUTE_REQUIRED_OPTIONS));
|
|
10
11
|
|
|
11
12
|
function rejectUnknownKeys(value, supported, label) {
|
|
12
13
|
const unknown = Object.keys(value).find(key => !supported.includes(key));
|
|
@@ -98,7 +99,7 @@ export function validateImputeTransform(transform) {
|
|
|
98
99
|
throw new Error(`Unsupported impute method "${transform.method}".`);
|
|
99
100
|
}
|
|
100
101
|
const hasValue = Object.hasOwn(transform, "value");
|
|
101
|
-
if (transform.method
|
|
102
|
+
if (IMPUTE_REQUIRED_OPTIONS[transform.method].includes("value")) {
|
|
102
103
|
if (!hasValue) throw new TypeError("Impute constant requires a value.");
|
|
103
104
|
if (transform.value === undefined) {
|
|
104
105
|
throw new TypeError("Impute constant value must be a JSON-safe scalar.");
|
|
@@ -26,6 +26,14 @@ export const COMPLETE_SCALE_TYPES = Object.freeze([
|
|
|
26
26
|
...new Set(Object.values(SCALE_TYPES_BY_ROLE).flat())
|
|
27
27
|
]);
|
|
28
28
|
|
|
29
|
+
// Dimensionless scale spacing is shared by generic and focused editors.
|
|
30
|
+
export const SCALE_OPTION_UNITS = Object.freeze({
|
|
31
|
+
padding: "band-fraction",
|
|
32
|
+
paddingInner: "band-fraction",
|
|
33
|
+
paddingOuter: "band-fraction",
|
|
34
|
+
align: "ratio"
|
|
35
|
+
});
|
|
36
|
+
|
|
29
37
|
const SCALE_PROPERTY_TYPES = Object.freeze({
|
|
30
38
|
midpoint: Object.freeze(["sequential"]),
|
|
31
39
|
nice: Object.freeze(["linear", "log", "pow", "sqrt", "symlog", "time"]),
|
|
@@ -376,7 +376,7 @@ function hasActiveStroke(properties) {
|
|
|
376
376
|
(properties.strokeWidth ?? 0) > 0;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
function primitiveBounds(type, properties = {}) {
|
|
379
|
+
function primitiveBounds(type, properties = {}, profile) {
|
|
380
380
|
const strokeExtent = hasActiveStroke(properties)
|
|
381
381
|
? properties.strokeWidth / 2
|
|
382
382
|
: 0;
|
|
@@ -420,7 +420,7 @@ function primitiveBounds(type, properties = {}) {
|
|
|
420
420
|
!Number.isFinite(properties.fontSize) ||
|
|
421
421
|
typeof properties.text !== "string"
|
|
422
422
|
) return undefined;
|
|
423
|
-
return resolveTextBounds(properties);
|
|
423
|
+
return resolveTextBounds(properties, profile);
|
|
424
424
|
}
|
|
425
425
|
if (type === "path") {
|
|
426
426
|
const commands = properties.commands;
|
|
@@ -469,15 +469,15 @@ function ancestorOffset(graphicSpec, target) {
|
|
|
469
469
|
}
|
|
470
470
|
}
|
|
471
471
|
|
|
472
|
-
function objectBounds(graphicSpec, id, object, ancestors, x, y) {
|
|
472
|
+
function objectBounds(graphicSpec, id, object, ancestors, x, y, profile) {
|
|
473
473
|
if (ancestors.has(id)) {
|
|
474
474
|
throw new Error(`Graphic attachment cycle includes "${id}".`);
|
|
475
475
|
}
|
|
476
476
|
ancestors.add(id);
|
|
477
477
|
let result = object.items === undefined
|
|
478
|
-
? translateBounds(primitiveBounds(object.type, object.properties), x, y)
|
|
478
|
+
? translateBounds(primitiveBounds(object.type, object.properties, profile), x, y)
|
|
479
479
|
: unionBounds(object.items.map(item => translateBounds(
|
|
480
|
-
primitiveBounds(item.type ?? object.type, item.properties),
|
|
480
|
+
primitiveBounds(item.type ?? object.type, item.properties, profile),
|
|
481
481
|
x,
|
|
482
482
|
y
|
|
483
483
|
)));
|
|
@@ -489,7 +489,7 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
|
|
|
489
489
|
throw new Error(`Unknown attached graphic "${childId}".`);
|
|
490
490
|
}
|
|
491
491
|
const bounds = objectBounds(
|
|
492
|
-
graphicSpec, childId, child.object, ancestors, childX, childY
|
|
492
|
+
graphicSpec, childId, child.object, ancestors, childX, childY, profile
|
|
493
493
|
);
|
|
494
494
|
if (bounds !== undefined) {
|
|
495
495
|
result = includeExtent(
|
|
@@ -501,23 +501,23 @@ function objectBounds(graphicSpec, id, object, ancestors, x, y) {
|
|
|
501
501
|
return result;
|
|
502
502
|
}
|
|
503
503
|
|
|
504
|
-
export function resolveConcreteGraphicBounds(graphicSpec, target) {
|
|
504
|
+
export function resolveConcreteGraphicBounds(graphicSpec, target, profile) {
|
|
505
505
|
const found = requireGraphic(graphicSpec, target);
|
|
506
506
|
const [x, y] = ancestorOffset(graphicSpec, target);
|
|
507
507
|
return requireFiniteBounds(found.kind === "item"
|
|
508
508
|
? translateBounds(
|
|
509
|
-
primitiveBounds(found.object.type ?? found.owner.type, found.object.properties),
|
|
509
|
+
primitiveBounds(found.object.type ?? found.owner.type, found.object.properties, profile),
|
|
510
510
|
x,
|
|
511
511
|
y
|
|
512
512
|
)
|
|
513
|
-
: objectBounds(graphicSpec, found.id, found.object, new Set(), x, y));
|
|
513
|
+
: objectBounds(graphicSpec, found.id, found.object, new Set(), x, y, profile));
|
|
514
514
|
}
|
|
515
515
|
|
|
516
|
-
export function unionConcreteGraphicBounds(graphicSpec, targets) {
|
|
516
|
+
export function unionConcreteGraphicBounds(graphicSpec, targets, profile) {
|
|
517
517
|
if (!Array.isArray(targets)) {
|
|
518
518
|
throw new TypeError("Graphic bounds targets must be an array.");
|
|
519
519
|
}
|
|
520
520
|
return unionBounds(targets.map(target =>
|
|
521
|
-
resolveConcreteGraphicBounds(graphicSpec, target)
|
|
521
|
+
resolveConcreteGraphicBounds(graphicSpec, target, profile)
|
|
522
522
|
));
|
|
523
523
|
}
|
package/src/layout/labels.js
CHANGED
|
@@ -172,7 +172,7 @@ export function assertMarkLabelPlacementSupport({ markType, coordinateType, inte
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function localTextBounds(text) {
|
|
175
|
-
return resolveTextBounds({ ...text, x: 0, y: 0 });
|
|
175
|
+
return resolveTextBounds({ ...text, x: 0, y: 0 }, text.profile);
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
function projectionRange(bounds, direction) {
|
|
@@ -207,7 +207,7 @@ function centerCandidate(center, text) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
function candidateBounds(candidate, text) {
|
|
210
|
-
return resolveTextBounds({ ...text, x: candidate.x, y: candidate.y });
|
|
210
|
+
return resolveTextBounds({ ...text, x: candidate.x, y: candidate.y }, text.profile);
|
|
211
211
|
}
|
|
212
212
|
|
|
213
213
|
function intervalContains(bounds, container) {
|
|
@@ -328,7 +328,8 @@ function outsideFallback(anchor) {
|
|
|
328
328
|
return anchor === "insideStart" ? "outsideStart" : "outsideEnd";
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
export function resolveMarkLabelPlacement({ placement, geometry, text } = {}) {
|
|
331
|
+
export function resolveMarkLabelPlacement({ placement, geometry, text } = {}, profile) {
|
|
332
|
+
text = { ...text, profile };
|
|
332
333
|
const normalized = normalizeMarkLabelPlacement(placement);
|
|
333
334
|
validatePlacementGeometry(geometry, normalized.anchor);
|
|
334
335
|
let anchor = normalized.anchor;
|
|
@@ -501,7 +502,7 @@ export function enumerateLabelOffsets({ axis, padding, maxDisplacement }) {
|
|
|
501
502
|
return cloneAndFreeze(candidates);
|
|
502
503
|
}
|
|
503
504
|
|
|
504
|
-
function textBounds(item, offset = { x: 0, y: 0 }) {
|
|
505
|
+
function textBounds(item, offset = { x: 0, y: 0 }, profile) {
|
|
505
506
|
return resolveTextBounds({
|
|
506
507
|
x: item.x + offset.x,
|
|
507
508
|
y: item.y + offset.y,
|
|
@@ -512,7 +513,7 @@ function textBounds(item, offset = { x: 0, y: 0 }) {
|
|
|
512
513
|
textAlign: item.textAlign,
|
|
513
514
|
textBaseline: item.textBaseline,
|
|
514
515
|
rotation: item.rotation
|
|
515
|
-
});
|
|
516
|
+
}, profile);
|
|
516
517
|
}
|
|
517
518
|
|
|
518
519
|
function candidateScore(bounds, boundary, placed, candidate, order) {
|
|
@@ -552,7 +553,7 @@ function overlapPairs(items) {
|
|
|
552
553
|
return pairs;
|
|
553
554
|
}
|
|
554
555
|
|
|
555
|
-
export function resolveLabelLayout({ items, bounds, ...options } = {}) {
|
|
556
|
+
export function resolveLabelLayout({ items, bounds, ...options } = {}, profile) {
|
|
556
557
|
if (!Array.isArray(items)) {
|
|
557
558
|
throw new TypeError("Label layout requires an item array.");
|
|
558
559
|
}
|
|
@@ -567,14 +568,14 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
|
|
|
567
568
|
);
|
|
568
569
|
const base = items.map(item => ({
|
|
569
570
|
id: item.id,
|
|
570
|
-
collisionBounds: expanded(textBounds(item), policy.padding)
|
|
571
|
+
collisionBounds: expanded(textBounds(item, undefined, profile), policy.padding)
|
|
571
572
|
}));
|
|
572
573
|
const placed = [];
|
|
573
574
|
const resolved = [];
|
|
574
575
|
for (const item of items) {
|
|
575
576
|
let best;
|
|
576
577
|
for (const [order, candidate] of candidates.entries()) {
|
|
577
|
-
const boundsAtCandidate = textBounds(item, candidate);
|
|
578
|
+
const boundsAtCandidate = textBounds(item, candidate, profile);
|
|
578
579
|
const collisionBounds = expanded(boundsAtCandidate, policy.padding);
|
|
579
580
|
const score = candidateScore(
|
|
580
581
|
collisionBounds,
|
|
@@ -619,7 +620,7 @@ export function resolveLabelLayout({ items, bounds, ...options } = {}) {
|
|
|
619
620
|
});
|
|
620
621
|
}
|
|
621
622
|
|
|
622
|
-
export function assertPolarTextLayout({ canvas, items, label }) {
|
|
623
|
+
export function assertPolarTextLayout({ canvas, items, label }, profile) {
|
|
623
624
|
const dimensions = canvas?.properties;
|
|
624
625
|
if (
|
|
625
626
|
![dimensions?.width, dimensions?.height].every(Number.isFinite) ||
|
|
@@ -627,7 +628,7 @@ export function assertPolarTextLayout({ canvas, items, label }) {
|
|
|
627
628
|
) {
|
|
628
629
|
throw new Error("Polar text layout requires finite Canvas dimensions.");
|
|
629
630
|
}
|
|
630
|
-
const bounds = items.map(resolveTextBounds);
|
|
631
|
+
const bounds = items.map(item => resolveTextBounds(item, profile));
|
|
631
632
|
const outside = bounds.some(item => !textBoundsFitCanvas(item, dimensions));
|
|
632
633
|
let overlap = false;
|
|
633
634
|
for (let first = 0; first < bounds.length && !overlap; first += 1) {
|
package/src/layout/legend.js
CHANGED
|
@@ -3,15 +3,15 @@ import {
|
|
|
3
3
|
measureTextWidth
|
|
4
4
|
} from "../core/textMetrics.js";
|
|
5
5
|
|
|
6
|
-
export function measureLegendTextWidth(value, style = { fontSize: 12 }) {
|
|
7
|
-
return measureTextWidth(formatVisibleText(value), style);
|
|
6
|
+
export function measureLegendTextWidth(value, style = { fontSize: 12 }, profile) {
|
|
7
|
+
return measureTextWidth(formatVisibleText(value), style, profile);
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export function resolveLegendGrid(config, width, count, symbolHeight) {
|
|
10
|
+
export function resolveLegendGrid(config, width, count, symbolHeight, profile) {
|
|
11
11
|
const labels = config.domain.map(formatVisibleText);
|
|
12
12
|
const itemWidths = labels.map(
|
|
13
13
|
label => width + config.labels.offset +
|
|
14
|
-
measureLegendTextWidth(label, config.labels)
|
|
14
|
+
measureLegendTextWidth(label, config.labels, profile)
|
|
15
15
|
);
|
|
16
16
|
const columnCount = Math.min(config.columns ?? count, count);
|
|
17
17
|
const rowCount = Math.ceil(count / columnCount);
|
|
@@ -3,7 +3,7 @@ import { alignLegendStart, resolveLegendGrid } from "./legend.js";
|
|
|
3
3
|
|
|
4
4
|
// Measure item content before choosing its edge. Family owners supply the
|
|
5
5
|
// sample dimensions and formatted labels; this module does not inspect scales.
|
|
6
|
-
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
6
|
+
export function resolveLegendItemLayout(plot, config, labels, symbol, canvas, profile) {
|
|
7
7
|
const strokes = labels.map((_, index) => Array.isArray(symbol.strokeWidth)
|
|
8
8
|
? symbol.strokeWidth[index] : symbol.strokeWidth ?? 0);
|
|
9
9
|
const sampleBounds = labels.map((_, index) => symbol.itemBounds?.[index] ?? {
|
|
@@ -19,17 +19,17 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
19
19
|
...sampleBounds.map(bounds => 2 * Math.max(-bounds.top, bounds.bottom)));
|
|
20
20
|
const side = ["left", "right"].includes(config.position);
|
|
21
21
|
const titleVisible = config.titleVisible !== false;
|
|
22
|
-
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle) : 0;
|
|
22
|
+
const titleWidth = titleVisible ? measureTextWidth(config.title, config.titleStyle, profile) : 0;
|
|
23
23
|
const titleHeight = titleVisible ? config.titleStyle.fontSize : 0;
|
|
24
24
|
const itemWidth = sampleWidth + config.labels.offset + Math.max(...labels.map(
|
|
25
|
-
text => measureTextWidth(text, config.labels)
|
|
25
|
+
text => measureTextWidth(text, config.labels, profile)
|
|
26
26
|
));
|
|
27
27
|
let symbolX;
|
|
28
28
|
let labelX;
|
|
29
29
|
let itemY;
|
|
30
30
|
let title;
|
|
31
31
|
if (config.layout === "legacy-bottom") {
|
|
32
|
-
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels));
|
|
32
|
+
const widths = labels.map(text => sampleWidth + config.labels.offset + measureTextWidth(text, config.labels, profile));
|
|
33
33
|
const width = widths.reduce((sum, value) => sum + value, 0) + config.itemGap * (labels.length - 1);
|
|
34
34
|
const x = config.align === "center" ? (canvas.width - width) / 2 : alignLegendStart(plot, width, config.align);
|
|
35
35
|
let cursor = x;
|
|
@@ -54,7 +54,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
54
54
|
itemY = labels.map((_, index) => firstY + index * pitch);
|
|
55
55
|
title = { x, y: plot.y + 20, align: "left" };
|
|
56
56
|
} else {
|
|
57
|
-
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight);
|
|
57
|
+
const grid = resolveLegendGrid({ ...config, domain: labels }, sampleWidth, labels.length, sampleHeight, profile);
|
|
58
58
|
const inline = config.titlePosition === "left" && titleVisible;
|
|
59
59
|
const prefix = inline ? titleWidth + 20 : 0;
|
|
60
60
|
const width = inline ? prefix + grid.gridWidth : Math.max(titleWidth, grid.gridWidth);
|
|
@@ -79,7 +79,7 @@ export function resolveLegendItemLayout(plot, config, labels, symbol, canvas) {
|
|
|
79
79
|
}
|
|
80
80
|
const textBounds = (x, y, text, style, align = "left") => resolveTextBounds({
|
|
81
81
|
x, y, text, ...style, textAlign: align, textBaseline: "middle"
|
|
82
|
-
});
|
|
82
|
+
}, profile);
|
|
83
83
|
const bounds = [
|
|
84
84
|
...(titleVisible ? [textBounds(title.x, title.y, config.title, config.titleStyle, title.align)] : []),
|
|
85
85
|
...labels.map((label, index) => textBounds(labelX[index], itemY[index], label, config.labels)),
|