ggaction 0.0.1
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 +27 -0
- package/LICENSE +21 -0
- package/README.md +108 -0
- package/package.json +84 -0
- package/src/AGENTS.md +119 -0
- package/src/ChartProgram.js +10 -0
- package/src/actions/boxPlots/components.js +88 -0
- package/src/actions/boxPlots/create.js +84 -0
- package/src/actions/boxPlots/index.js +10 -0
- package/src/actions/boxPlots/materialize.js +183 -0
- package/src/actions/boxPlots/options.js +147 -0
- package/src/actions/boxPlots/resolve.js +38 -0
- package/src/actions/canvas/actions.js +144 -0
- package/src/actions/canvas/index.js +6 -0
- package/src/actions/coordinates/actions.js +85 -0
- package/src/actions/coordinates/index.js +5 -0
- package/src/actions/data/box.js +40 -0
- package/src/actions/data/create.js +33 -0
- package/src/actions/data/density.js +62 -0
- package/src/actions/data/derived.js +52 -0
- package/src/actions/data/filter.js +168 -0
- package/src/actions/data/index.js +32 -0
- package/src/actions/data/interval.js +69 -0
- package/src/actions/data/regression.js +71 -0
- package/src/actions/data/shared.js +25 -0
- package/src/actions/encodings/appearance.js +243 -0
- package/src/actions/encodings/barWidth.js +60 -0
- package/src/actions/encodings/color.js +329 -0
- package/src/actions/encodings/density.js +225 -0
- package/src/actions/encodings/histogram.js +50 -0
- package/src/actions/encodings/index.js +23 -0
- package/src/actions/encodings/offset.js +104 -0
- package/src/actions/encodings/position/policies/area.js +24 -0
- package/src/actions/encodings/position/policies/bar.js +110 -0
- package/src/actions/encodings/position/policies/common.js +17 -0
- package/src/actions/encodings/position/policies/index.js +25 -0
- package/src/actions/encodings/position/policies/line.js +74 -0
- package/src/actions/encodings/position/policies/point.js +19 -0
- package/src/actions/encodings/position/policies/rule.js +17 -0
- package/src/actions/encodings/position/resolve.js +170 -0
- package/src/actions/encodings/position.js +176 -0
- package/src/actions/encodings/ranged.js +276 -0
- package/src/actions/encodings/ruleAppearance.js +47 -0
- package/src/actions/encodings/shared.js +164 -0
- package/src/actions/encodings/strokeDash.js +145 -0
- package/src/actions/errorBands/components.js +81 -0
- package/src/actions/errorBands/create.js +159 -0
- package/src/actions/errorBands/index.js +7 -0
- package/src/actions/errorBands/resolve.js +28 -0
- package/src/actions/errorBars/components.js +62 -0
- package/src/actions/errorBars/create.js +144 -0
- package/src/actions/errorBars/index.js +7 -0
- package/src/actions/errorBars/resolve.js +25 -0
- package/src/actions/guides/axes/axes.js +204 -0
- package/src/actions/guides/axes/axis.js +131 -0
- package/src/actions/guides/axes/index.js +17 -0
- package/src/actions/guides/axes/labels.js +231 -0
- package/src/actions/guides/axes/lines.js +149 -0
- package/src/actions/guides/axes/policy.js +155 -0
- package/src/actions/guides/axes/tickGroups.js +149 -0
- package/src/actions/guides/axes/ticks.js +179 -0
- package/src/actions/guides/axes/titles.js +264 -0
- package/src/actions/guides/grids/grid.js +228 -0
- package/src/actions/guides/grids/index.js +1 -0
- package/src/actions/guides/grids/resolve.js +258 -0
- package/src/actions/guides/guides.js +138 -0
- package/src/actions/guides/index.js +11 -0
- package/src/actions/guides/legends/categorical/actions.js +329 -0
- package/src/actions/guides/legends/categorical/components.js +140 -0
- package/src/actions/guides/legends/categorical/index.js +49 -0
- package/src/actions/guides/legends/categorical/layout.js +445 -0
- package/src/actions/guides/legends/categorical/options.js +188 -0
- package/src/actions/guides/legends/categorical/recipes.js +120 -0
- package/src/actions/guides/legends/categorical/resolve.js +131 -0
- package/src/actions/guides/legends/categorical/symbols.js +280 -0
- package/src/actions/guides/legends/categorical/validation.js +27 -0
- package/src/actions/guides/legends/continuous/common.js +273 -0
- package/src/actions/guides/legends/continuous/gradient.js +363 -0
- package/src/actions/guides/legends/continuous/index.js +25 -0
- package/src/actions/guides/legends/continuous/interval.js +234 -0
- package/src/actions/guides/legends/continuous/opacity.js +382 -0
- package/src/actions/guides/legends/edit.js +314 -0
- package/src/actions/guides/legends/index.js +9 -0
- package/src/actions/guides/legends/size.js +220 -0
- package/src/actions/guides/tickValues.js +84 -0
- package/src/actions/index.js +31 -0
- package/src/actions/intervals/resolve.js +352 -0
- package/src/actions/marks/area.js +369 -0
- package/src/actions/marks/bar/create.js +46 -0
- package/src/actions/marks/bar/edit.js +90 -0
- package/src/actions/marks/bar/index.js +9 -0
- package/src/actions/marks/bar/materialize.js +130 -0
- package/src/actions/marks/index.js +13 -0
- package/src/actions/marks/line.js +248 -0
- package/src/actions/marks/point.js +420 -0
- package/src/actions/marks/rule.js +287 -0
- package/src/actions/marks/shared.js +112 -0
- package/src/actions/primitives/createGraphics.js +163 -0
- package/src/actions/primitives/editGraphics.js +208 -0
- package/src/actions/primitives/index.js +9 -0
- package/src/actions/primitives/semantic.js +168 -0
- package/src/actions/primitives/semanticValue.js +182 -0
- package/src/actions/regression/components.js +216 -0
- package/src/actions/regression/create.js +102 -0
- package/src/actions/regression/index.js +23 -0
- package/src/actions/regression/resolve.js +57 -0
- package/src/actions/scales/consumers.js +283 -0
- package/src/actions/scales/create.js +148 -0
- package/src/actions/scales/definitions.js +319 -0
- package/src/actions/scales/edit.js +290 -0
- package/src/actions/scales/index.js +11 -0
- package/src/actions/scales/materialize.js +466 -0
- package/src/actions/scales/quantitativeColor.js +45 -0
- package/src/actions/selection/actions.js +384 -0
- package/src/actions/selection/index.js +23 -0
- package/src/actions/titles/actions.js +235 -0
- package/src/actions/titles/index.js +19 -0
- package/src/actions/titles/resolve.js +321 -0
- package/src/core/ChartProgram.js +316 -0
- package/src/core/action.js +120 -0
- package/src/core/identifiers.js +28 -0
- package/src/core/immutable.js +94 -0
- package/src/core/specs.js +29 -0
- package/src/core/textMetrics.js +23 -0
- package/src/core/validation.js +54 -0
- package/src/core/vocabulary.js +58 -0
- package/src/extension.js +2 -0
- package/src/grammar/aggregate.js +259 -0
- package/src/grammar/areaSeries.js +217 -0
- package/src/grammar/bars/aggregate.js +98 -0
- package/src/grammar/bars/geometry.js +54 -0
- package/src/grammar/bars/policy.js +90 -0
- package/src/grammar/boxPlot.js +140 -0
- package/src/grammar/coordinates.js +25 -0
- package/src/grammar/curveCommands.js +356 -0
- package/src/grammar/density.js +296 -0
- package/src/grammar/filter.js +169 -0
- package/src/grammar/histogram.js +332 -0
- package/src/grammar/interval.js +343 -0
- package/src/grammar/lineSeries.js +197 -0
- package/src/grammar/markFilter.js +46 -0
- package/src/grammar/markSelection.js +326 -0
- package/src/grammar/palettes.js +208 -0
- package/src/grammar/pathCommands.js +86 -0
- package/src/grammar/pointShapes.js +164 -0
- package/src/grammar/positionCompatibility.js +32 -0
- package/src/grammar/regression.js +578 -0
- package/src/grammar/ruleAppearance.js +12 -0
- package/src/grammar/rules.js +90 -0
- package/src/grammar/scales/appearance.js +185 -0
- package/src/grammar/scales/color.js +349 -0
- package/src/grammar/scales/continuous.js +111 -0
- package/src/grammar/scales/definition.js +153 -0
- package/src/grammar/scales/discretized.js +159 -0
- package/src/grammar/scales/fields.js +133 -0
- package/src/grammar/scales/index.js +12 -0
- package/src/grammar/scales/mapping.js +27 -0
- package/src/grammar/scales/ordinal.js +221 -0
- package/src/grammar/scales/policies.js +50 -0
- package/src/grammar/scales/temporal.js +53 -0
- package/src/grammar/scales/transformed.js +236 -0
- package/src/grammar/scales/types.js +102 -0
- package/src/grammar/scales/validation.js +66 -0
- package/src/grammar/scales.js +1 -0
- package/src/grammar/schemas/concreteGraphic.js +73 -0
- package/src/grammar/schemas/graphic.js +131 -0
- package/src/grammar/schemas/graphicBounds.js +146 -0
- package/src/grammar/schemas/graphicTree.js +133 -0
- package/src/grammar/schemas/mark.js +4 -0
- package/src/grammar/schemas/semanticPath.js +174 -0
- package/src/grammar/seriesLayout.js +76 -0
- package/src/grammar/ticks.js +149 -0
- package/src/grammar/transforms.js +37 -0
- package/src/index.js +2 -0
- package/src/layout/canvas.js +114 -0
- package/src/layout/grid.js +17 -0
- package/src/layout/legend.js +51 -0
- package/src/layout/text.js +62 -0
- package/src/layout/title.js +107 -0
- package/src/materialization/bars/aggregate.js +184 -0
- package/src/materialization/bars/grouped.js +117 -0
- package/src/materialization/bars/histogram.js +163 -0
- package/src/materialization/bars/ranged.js +53 -0
- package/src/materialization/bars/resolve.js +56 -0
- package/src/materialization/dataProvenance.js +15 -0
- package/src/materialization/density.js +29 -0
- package/src/materialization/dependencies.js +134 -0
- package/src/materialization/encodings.js +50 -0
- package/src/materialization/graphicHierarchy.js +104 -0
- package/src/materialization/legends.js +20 -0
- package/src/materialization/marks.js +110 -0
- package/src/materialization/planner.js +38 -0
- package/src/materialization/selection/filter.js +40 -0
- package/src/materialization/selection/items.js +503 -0
- package/src/materialization/selection/path.js +37 -0
- package/src/materialization/selection/point.js +118 -0
- package/src/materialization/selection/policies/area.js +10 -0
- package/src/materialization/selection/policies/bar.js +10 -0
- package/src/materialization/selection/policies/index.js +40 -0
- package/src/materialization/selection/policies/line.js +10 -0
- package/src/materialization/selection/policies/point.js +10 -0
- package/src/materialization/selection/policies/rule.js +10 -0
- package/src/materialization/selection/state.js +67 -0
- package/src/materialization/selection/styles.js +183 -0
- package/src/renderers/canvas/circle.js +62 -0
- package/src/renderers/canvas/index.js +132 -0
- package/src/renderers/canvas/line.js +54 -0
- package/src/renderers/canvas/path.js +98 -0
- package/src/renderers/canvas/rect.js +57 -0
- package/src/renderers/canvas/text.js +96 -0
- package/src/renderers/canvas/validation.js +23 -0
- package/src/renderers/png.js +29 -0
- package/src/selectors/coordinates.js +13 -0
- package/src/selectors/datasets.js +13 -0
- package/src/selectors/index.js +4 -0
- package/src/selectors/layers.js +32 -0
- package/src/selectors/scales.js +21 -0
- package/src/theme/defaults.js +13 -0
- package/types/extension.d.ts +14 -0
- package/types/index.d.ts +81 -0
- package/types/png.d.ts +14 -0
- package/types/program.d.ts +1096 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { findDataset } from "../../../selectors/datasets.js";
|
|
2
|
+
import { requireLayer } from "../../../selectors/layers.js";
|
|
3
|
+
import { areaSelectionPolicy } from "./area.js";
|
|
4
|
+
import { barSelectionPolicy } from "./bar.js";
|
|
5
|
+
import { lineSelectionPolicy } from "./line.js";
|
|
6
|
+
import { pointSelectionPolicy } from "./point.js";
|
|
7
|
+
import { ruleSelectionPolicy } from "./rule.js";
|
|
8
|
+
|
|
9
|
+
const POLICIES = Object.freeze({
|
|
10
|
+
area: areaSelectionPolicy,
|
|
11
|
+
bar: barSelectionPolicy,
|
|
12
|
+
line: lineSelectionPolicy,
|
|
13
|
+
point: pointSelectionPolicy,
|
|
14
|
+
rule: ruleSelectionPolicy
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function findSelectionPolicy(markType) {
|
|
18
|
+
return POLICIES[markType];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function requireSelectionPolicy(markType) {
|
|
22
|
+
const policy = findSelectionPolicy(markType);
|
|
23
|
+
if (policy === undefined) {
|
|
24
|
+
throw new Error(`Mark type "${markType}" has no selection policy.`);
|
|
25
|
+
}
|
|
26
|
+
return policy;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveMarkItems(program, target, grain = "item") {
|
|
30
|
+
const layer = requireLayer(program, target, `Unknown mark target "${target}"`);
|
|
31
|
+
const dataset = findDataset(program, layer.data);
|
|
32
|
+
if (dataset === undefined) {
|
|
33
|
+
throw new Error(`Mark "${target}" requires an existing dataset for selection.`);
|
|
34
|
+
}
|
|
35
|
+
const policy = requireSelectionPolicy(layer.mark?.type);
|
|
36
|
+
if (!policy.supportedGrains.includes(grain)) {
|
|
37
|
+
throw new Error(`Mark "${target}" does not support ${grain} selection grain.`);
|
|
38
|
+
}
|
|
39
|
+
return policy.resolveItems(program, layer, dataset, grain);
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveLineItems } from "../items.js";
|
|
2
|
+
import { normalizeStrokeHighlightStyle } from "../styles.js";
|
|
3
|
+
|
|
4
|
+
export const lineSelectionPolicy = Object.freeze({
|
|
5
|
+
supportedGrains: Object.freeze(["item"]),
|
|
6
|
+
resolveItems: resolveLineItems,
|
|
7
|
+
normalizeHighlightStyle: args => normalizeStrokeHighlightStyle(args, "Line"),
|
|
8
|
+
applyHighlightOp: "applyPathHighlight",
|
|
9
|
+
rematerializeOp: "rematerializeLineMark"
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolvePointItems } from "../items.js";
|
|
2
|
+
import { normalizePointHighlightStyle } from "../styles.js";
|
|
3
|
+
|
|
4
|
+
export const pointSelectionPolicy = Object.freeze({
|
|
5
|
+
supportedGrains: Object.freeze(["item"]),
|
|
6
|
+
resolveItems: resolvePointItems,
|
|
7
|
+
normalizeHighlightStyle: normalizePointHighlightStyle,
|
|
8
|
+
applyHighlightOp: "applyPointHighlight",
|
|
9
|
+
rematerializeOp: "rematerializePointMark"
|
|
10
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { resolveRuleItems } from "../items.js";
|
|
2
|
+
import { normalizeStrokeHighlightStyle } from "../styles.js";
|
|
3
|
+
|
|
4
|
+
export const ruleSelectionPolicy = Object.freeze({
|
|
5
|
+
supportedGrains: Object.freeze(["item"]),
|
|
6
|
+
resolveItems: resolveRuleItems,
|
|
7
|
+
normalizeHighlightStyle: args => normalizeStrokeHighlightStyle(args, "Rule"),
|
|
8
|
+
applyHighlightOp: "applyRuleHighlight",
|
|
9
|
+
rematerializeOp: "rematerializeRuleMark"
|
|
10
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { validateUserId } from "../../core/identifiers.js";
|
|
2
|
+
import { normalizeMarkSelector, selectMarkItemKeys } from "../../grammar/markSelection.js";
|
|
3
|
+
import { resolveMarkItems } from "./policies/index.js";
|
|
4
|
+
|
|
5
|
+
export function resolveMarkSelection(program, target, selector) {
|
|
6
|
+
const normalized = normalizeMarkSelector(selector);
|
|
7
|
+
const items = resolveMarkItems(program, target, normalized.grain);
|
|
8
|
+
if (items.length > 0) {
|
|
9
|
+
const source = normalized.field !== undefined
|
|
10
|
+
? "fields"
|
|
11
|
+
: normalized.channel !== undefined
|
|
12
|
+
? "channels"
|
|
13
|
+
: "properties";
|
|
14
|
+
const kind = normalized.field !== undefined
|
|
15
|
+
? "field"
|
|
16
|
+
: normalized.channel !== undefined
|
|
17
|
+
? "channel"
|
|
18
|
+
: "graphic property";
|
|
19
|
+
const key = normalized.field ?? normalized.channel ?? normalized.property;
|
|
20
|
+
if (!items.some(item => Object.hasOwn(item[source], key))) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
`Selection ${kind} "${key}" is not uniquely defined at the target ${normalized.grain} grain.`
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return Object.freeze({
|
|
27
|
+
selector: normalized,
|
|
28
|
+
items,
|
|
29
|
+
keys: selectMarkItemKeys(items, normalized)
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveSelectionCreationId(program, id, target) {
|
|
34
|
+
const resolved = validateUserId(
|
|
35
|
+
id ?? `${target}Selection`,
|
|
36
|
+
"Selection id"
|
|
37
|
+
);
|
|
38
|
+
if (program.materializationConfigs.selections?.[resolved] !== undefined) {
|
|
39
|
+
throw new Error(`Selection "${resolved}" already exists.`);
|
|
40
|
+
}
|
|
41
|
+
return resolved;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function resolveStoredSelection(program, id) {
|
|
45
|
+
const selections = program.materializationConfigs.selections ?? {};
|
|
46
|
+
const requested = id ?? program.context.currentSelection;
|
|
47
|
+
const resolvedId = requested ?? (
|
|
48
|
+
Object.keys(selections).length === 1 ? Object.keys(selections)[0] : undefined
|
|
49
|
+
);
|
|
50
|
+
if (resolvedId === undefined) {
|
|
51
|
+
if (Object.keys(selections).length === 0) {
|
|
52
|
+
throw new Error("Selection resolution requires an existing selection.");
|
|
53
|
+
}
|
|
54
|
+
throw new Error("Selection is ambiguous; provide selection.");
|
|
55
|
+
}
|
|
56
|
+
validateUserId(resolvedId, "Selection id");
|
|
57
|
+
const definition = selections[resolvedId];
|
|
58
|
+
if (definition === undefined) {
|
|
59
|
+
throw new Error(`Unknown selection "${resolvedId}".`);
|
|
60
|
+
}
|
|
61
|
+
const resolved = resolveMarkSelection(
|
|
62
|
+
program,
|
|
63
|
+
definition.target,
|
|
64
|
+
definition.selector
|
|
65
|
+
);
|
|
66
|
+
return Object.freeze({ id: resolvedId, definition, ...resolved });
|
|
67
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateKeys,
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validatePositiveFinite,
|
|
7
|
+
validateUnitInterval
|
|
8
|
+
} from "../../core/validation.js";
|
|
9
|
+
import { validatePointShape } from "../../grammar/pointShapes.js";
|
|
10
|
+
import { normalizeStrokeDashPattern } from "../../grammar/scales.js";
|
|
11
|
+
import { DEFAULT_COLORS } from "../../theme/defaults.js";
|
|
12
|
+
|
|
13
|
+
const DEFAULT_POINT_SIZE = 2;
|
|
14
|
+
const DEFAULT_DIM_OPACITY = 0.25;
|
|
15
|
+
|
|
16
|
+
export { validateUnitInterval };
|
|
17
|
+
|
|
18
|
+
function normalizeOffset(offset) {
|
|
19
|
+
if (offset === undefined) return { x: 0, y: 0 };
|
|
20
|
+
if (!isPlainObject(offset)) {
|
|
21
|
+
throw new TypeError("Highlight offset must be a plain object.");
|
|
22
|
+
}
|
|
23
|
+
validateKeys(offset, ["x", "y"], "highlight offset");
|
|
24
|
+
const x = offset.x ?? 0;
|
|
25
|
+
const y = offset.y ?? 0;
|
|
26
|
+
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
27
|
+
throw new TypeError("Highlight offset x and y must be finite numbers.");
|
|
28
|
+
}
|
|
29
|
+
return { x, y };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function normalizeDimOthers(dimOthers) {
|
|
33
|
+
if (dimOthers === undefined || dimOthers === false) return false;
|
|
34
|
+
if (dimOthers === true) return { opacity: DEFAULT_DIM_OPACITY };
|
|
35
|
+
if (!isPlainObject(dimOthers)) {
|
|
36
|
+
throw new TypeError("Highlight dimOthers must be a boolean or plain object.");
|
|
37
|
+
}
|
|
38
|
+
validateKeys(dimOthers, ["opacity"], "highlight dimOthers");
|
|
39
|
+
return {
|
|
40
|
+
opacity: validateUnitInterval(
|
|
41
|
+
dimOthers.opacity ?? DEFAULT_DIM_OPACITY,
|
|
42
|
+
"Highlight dim opacity"
|
|
43
|
+
)
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function normalizePointHighlightStyle(args) {
|
|
48
|
+
if (args.color !== undefined && args.fill !== undefined) {
|
|
49
|
+
throw new Error("Point highlight accepts color or fill, not both.");
|
|
50
|
+
}
|
|
51
|
+
if (args.strokeDash !== undefined) {
|
|
52
|
+
throw new Error("Point highlight does not support strokeDash.");
|
|
53
|
+
}
|
|
54
|
+
const fill = validateNonEmptyString(
|
|
55
|
+
args.fill ?? args.color ?? DEFAULT_COLORS.highlight,
|
|
56
|
+
"Point highlight fill"
|
|
57
|
+
);
|
|
58
|
+
const opacity = args.opacity === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: validateUnitInterval(args.opacity, "Point highlight opacity");
|
|
61
|
+
const stroke = args.stroke === undefined
|
|
62
|
+
? undefined
|
|
63
|
+
: validateNonEmptyString(args.stroke, "Point highlight stroke");
|
|
64
|
+
const strokeWidth = args.strokeWidth === undefined
|
|
65
|
+
? undefined
|
|
66
|
+
: validateNonNegativeFinite(args.strokeWidth, "Point highlight strokeWidth");
|
|
67
|
+
if (strokeWidth !== undefined && stroke === undefined) {
|
|
68
|
+
throw new Error("Point highlight strokeWidth requires stroke.");
|
|
69
|
+
}
|
|
70
|
+
return {
|
|
71
|
+
fill,
|
|
72
|
+
...(opacity === undefined ? {} : { opacity }),
|
|
73
|
+
...(stroke === undefined ? {} : { stroke }),
|
|
74
|
+
...(strokeWidth === undefined ? {} : { strokeWidth }),
|
|
75
|
+
...(args.shape === undefined ? {} : { shape: validatePointShape(args.shape) }),
|
|
76
|
+
size: args.size === undefined
|
|
77
|
+
? DEFAULT_POINT_SIZE
|
|
78
|
+
: validatePositiveFinite(args.size, "Point highlight size"),
|
|
79
|
+
offset: normalizeOffset(args.offset)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeBarHighlightStyle(args) {
|
|
84
|
+
for (const option of ["shape", "size", "offset", "strokeDash"]) {
|
|
85
|
+
if (args[option] !== undefined) {
|
|
86
|
+
throw new Error(`Bar highlight does not support ${option}.`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (args.color !== undefined && args.fill !== undefined) {
|
|
90
|
+
throw new Error("Bar highlight accepts color or fill, not both.");
|
|
91
|
+
}
|
|
92
|
+
const fill = validateNonEmptyString(
|
|
93
|
+
args.fill ?? args.color ?? DEFAULT_COLORS.highlight,
|
|
94
|
+
"Bar highlight fill"
|
|
95
|
+
);
|
|
96
|
+
const opacity = args.opacity === undefined
|
|
97
|
+
? undefined
|
|
98
|
+
: validateUnitInterval(args.opacity, "Bar highlight opacity");
|
|
99
|
+
const stroke = args.stroke === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: validateNonEmptyString(args.stroke, "Bar highlight stroke");
|
|
102
|
+
const strokeWidth = args.strokeWidth === undefined
|
|
103
|
+
? undefined
|
|
104
|
+
: validateNonNegativeFinite(args.strokeWidth, "Bar highlight strokeWidth");
|
|
105
|
+
if (strokeWidth !== undefined && stroke === undefined) {
|
|
106
|
+
throw new Error("Bar highlight strokeWidth requires stroke.");
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
fill,
|
|
110
|
+
...(opacity === undefined ? {} : { opacity }),
|
|
111
|
+
...(stroke === undefined ? {} : { stroke }),
|
|
112
|
+
...(strokeWidth === undefined ? {} : { strokeWidth })
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function rejectHighlightOptions(args, mark, options) {
|
|
117
|
+
for (const option of options) {
|
|
118
|
+
if (args[option] !== undefined) {
|
|
119
|
+
throw new Error(`${mark} highlight does not support ${option}.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeStrokeHighlightStyle(args, mark) {
|
|
125
|
+
rejectHighlightOptions(args, mark, ["fill", "shape", "size"]);
|
|
126
|
+
if (args.color !== undefined && args.stroke !== undefined) {
|
|
127
|
+
throw new Error(`${mark} highlight accepts color or stroke, not both.`);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
stroke: validateNonEmptyString(
|
|
131
|
+
args.stroke ?? args.color ?? DEFAULT_COLORS.highlight,
|
|
132
|
+
`${mark} highlight stroke`
|
|
133
|
+
),
|
|
134
|
+
...(args.strokeWidth === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
strokeWidth: validateNonNegativeFinite(
|
|
138
|
+
args.strokeWidth,
|
|
139
|
+
`${mark} highlight strokeWidth`
|
|
140
|
+
)
|
|
141
|
+
}),
|
|
142
|
+
...(args.strokeDash === undefined
|
|
143
|
+
? {}
|
|
144
|
+
: { strokeDash: normalizeStrokeDashPattern(args.strokeDash) }),
|
|
145
|
+
...(args.opacity === undefined
|
|
146
|
+
? {}
|
|
147
|
+
: {
|
|
148
|
+
opacity: validateUnitInterval(
|
|
149
|
+
args.opacity,
|
|
150
|
+
`${mark} highlight opacity`
|
|
151
|
+
)
|
|
152
|
+
}),
|
|
153
|
+
offset: normalizeOffset(args.offset)
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeAreaHighlightStyle(args) {
|
|
158
|
+
rejectHighlightOptions(args, "Area", ["shape", "size", "strokeDash"]);
|
|
159
|
+
if (args.color !== undefined && args.fill !== undefined) {
|
|
160
|
+
throw new Error("Area highlight accepts color or fill, not both.");
|
|
161
|
+
}
|
|
162
|
+
const stroke = args.stroke === undefined
|
|
163
|
+
? undefined
|
|
164
|
+
: validateNonEmptyString(args.stroke, "Area highlight stroke");
|
|
165
|
+
const strokeWidth = args.strokeWidth === undefined
|
|
166
|
+
? undefined
|
|
167
|
+
: validateNonNegativeFinite(args.strokeWidth, "Area highlight strokeWidth");
|
|
168
|
+
if (strokeWidth !== undefined && stroke === undefined) {
|
|
169
|
+
throw new Error("Area highlight strokeWidth requires stroke.");
|
|
170
|
+
}
|
|
171
|
+
return {
|
|
172
|
+
fill: validateNonEmptyString(
|
|
173
|
+
args.fill ?? args.color ?? DEFAULT_COLORS.highlight,
|
|
174
|
+
"Area highlight fill"
|
|
175
|
+
),
|
|
176
|
+
...(args.opacity === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { opacity: validateUnitInterval(args.opacity, "Area highlight opacity") }),
|
|
179
|
+
...(stroke === undefined ? {} : { stroke }),
|
|
180
|
+
...(strokeWidth === undefined ? {} : { strokeWidth }),
|
|
181
|
+
offset: normalizeOffset(args.offset)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { requireFiniteProperty } from "./validation.js";
|
|
2
|
+
import { validateConcreteGraphicProperties } from
|
|
3
|
+
"../../grammar/schemas/concreteGraphic.js";
|
|
4
|
+
|
|
5
|
+
function drawCircle(context, child, collectionId) {
|
|
6
|
+
const properties = child.properties ?? {};
|
|
7
|
+
const graphicId = child.id ?? collectionId;
|
|
8
|
+
validateConcreteGraphicProperties("circle", properties);
|
|
9
|
+
const x = requireFiniteProperty(properties, "x", graphicId);
|
|
10
|
+
const y = requireFiniteProperty(properties, "y", graphicId);
|
|
11
|
+
const radius = requireFiniteProperty(properties, "radius", graphicId);
|
|
12
|
+
|
|
13
|
+
if (radius < 0) {
|
|
14
|
+
throw new Error(`Graphic "${graphicId}" requires a non-negative radius.`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof properties.fill !== "string") {
|
|
18
|
+
throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const opacity = properties.opacity ?? 1;
|
|
22
|
+
|
|
23
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
|
|
24
|
+
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
context.fillStyle = properties.fill;
|
|
28
|
+
context.globalAlpha = opacity;
|
|
29
|
+
context.beginPath();
|
|
30
|
+
context.arc(x, y, radius, 0, Math.PI * 2);
|
|
31
|
+
context.fill();
|
|
32
|
+
if (properties.stroke !== undefined) {
|
|
33
|
+
if (typeof properties.stroke !== "string") {
|
|
34
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
35
|
+
}
|
|
36
|
+
const strokeWidth = requireFiniteProperty(
|
|
37
|
+
properties,
|
|
38
|
+
"strokeWidth",
|
|
39
|
+
graphicId
|
|
40
|
+
);
|
|
41
|
+
if (strokeWidth < 0) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
context.strokeStyle = properties.stroke;
|
|
47
|
+
context.lineWidth = strokeWidth;
|
|
48
|
+
context.setLineDash([]);
|
|
49
|
+
context.stroke();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function drawCircleGraphic(context, id, graphic) {
|
|
54
|
+
if (graphic.items) {
|
|
55
|
+
for (const item of graphic.items) {
|
|
56
|
+
drawCircle(context, item, id);
|
|
57
|
+
}
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
drawCircle(context, { id, properties: graphic.properties }, id);
|
|
62
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { drawCircleGraphic } from "./circle.js";
|
|
2
|
+
import { drawLineGraphic } from "./line.js";
|
|
3
|
+
import { drawPathGraphic } from "./path.js";
|
|
4
|
+
import { drawRectGraphic } from "./rect.js";
|
|
5
|
+
import { drawTextGraphic } from "./text.js";
|
|
6
|
+
import { requireFiniteProperty } from "./validation.js";
|
|
7
|
+
import { walkGraphicDrawOrder } from "../../grammar/schemas/graphicTree.js";
|
|
8
|
+
|
|
9
|
+
const DRAWERS = Object.freeze({
|
|
10
|
+
circle: drawCircleGraphic,
|
|
11
|
+
rect: drawRectGraphic,
|
|
12
|
+
line: drawLineGraphic,
|
|
13
|
+
text: drawTextGraphic,
|
|
14
|
+
path: drawPathGraphic
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
function requireCanvasContext(context) {
|
|
18
|
+
const methods = [
|
|
19
|
+
"save",
|
|
20
|
+
"restore",
|
|
21
|
+
"clearRect",
|
|
22
|
+
"fillRect",
|
|
23
|
+
"beginPath",
|
|
24
|
+
"closePath",
|
|
25
|
+
"arc",
|
|
26
|
+
"fill",
|
|
27
|
+
"moveTo",
|
|
28
|
+
"lineTo",
|
|
29
|
+
"bezierCurveTo",
|
|
30
|
+
"setLineDash",
|
|
31
|
+
"stroke",
|
|
32
|
+
"translate",
|
|
33
|
+
"rotate",
|
|
34
|
+
"fillText",
|
|
35
|
+
"scale"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
if (context === null || typeof context !== "object" || !context.canvas) {
|
|
39
|
+
throw new TypeError("render requires a Canvas 2D context.");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
for (const method of methods) {
|
|
43
|
+
if (typeof context[method] !== "function") {
|
|
44
|
+
throw new TypeError(`Canvas context is missing ${method}().`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function findCanvas(graphicSpec) {
|
|
50
|
+
const canvasIds = graphicSpec.order.filter(
|
|
51
|
+
id => graphicSpec.objects[id]?.type === "canvas"
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
if (canvasIds.length !== 1) {
|
|
55
|
+
throw new Error("graphicSpec must contain exactly one ordered canvas.");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { id: canvasIds[0], graphic: graphicSpec.objects[canvasIds[0]] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function render(program, context, { pixelRatio = 1 } = {}) {
|
|
62
|
+
const graphicSpec = program?.graphicSpec;
|
|
63
|
+
|
|
64
|
+
if (
|
|
65
|
+
graphicSpec === null ||
|
|
66
|
+
typeof graphicSpec !== "object" ||
|
|
67
|
+
graphicSpec.objects === null ||
|
|
68
|
+
typeof graphicSpec.objects !== "object" ||
|
|
69
|
+
!Array.isArray(graphicSpec.order)
|
|
70
|
+
) {
|
|
71
|
+
throw new TypeError("render requires a program with a graphicSpec.");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
requireCanvasContext(context);
|
|
75
|
+
|
|
76
|
+
if (!Number.isFinite(pixelRatio) || pixelRatio <= 0) {
|
|
77
|
+
throw new RangeError("render pixelRatio must be a positive finite number.");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { id: canvasId, graphic: canvas } = findCanvas(graphicSpec);
|
|
81
|
+
const width = requireFiniteProperty(canvas.properties ?? {}, "width", canvasId);
|
|
82
|
+
const height = requireFiniteProperty(
|
|
83
|
+
canvas.properties ?? {},
|
|
84
|
+
"height",
|
|
85
|
+
canvasId
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
if (width < 0 || height < 0) {
|
|
89
|
+
throw new Error("Canvas width and height must not be negative.");
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
context.canvas.width = Math.round(width * pixelRatio);
|
|
93
|
+
context.canvas.height = Math.round(height * pixelRatio);
|
|
94
|
+
context.save();
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
context.scale(pixelRatio, pixelRatio);
|
|
98
|
+
context.clearRect(0, 0, width, height);
|
|
99
|
+
|
|
100
|
+
if (canvas.properties.background !== undefined) {
|
|
101
|
+
if (typeof canvas.properties.background !== "string") {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Graphic "${canvasId}" requires a string background property.`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
context.globalAlpha = 1;
|
|
108
|
+
context.fillStyle = canvas.properties.background;
|
|
109
|
+
context.fillRect(0, 0, width, height);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
walkGraphicDrawOrder(graphicSpec, ({ id, object }) => {
|
|
113
|
+
if (id !== canvasId) drawConcreteGraphic(context, id, object);
|
|
114
|
+
});
|
|
115
|
+
} finally {
|
|
116
|
+
context.restore();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function drawConcreteGraphic(context, id, graphic) {
|
|
121
|
+
if (graphic.type === "collection") {
|
|
122
|
+
for (const item of graphic.items ?? []) {
|
|
123
|
+
drawConcreteGraphic(context, item.id ?? id, item);
|
|
124
|
+
}
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const draw = DRAWERS[graphic.type];
|
|
128
|
+
if (draw === undefined) {
|
|
129
|
+
throw new Error(`Canvas renderer does not support "${graphic.type}" yet.`);
|
|
130
|
+
}
|
|
131
|
+
draw(context, id, graphic);
|
|
132
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { requireFiniteProperty } from "./validation.js";
|
|
2
|
+
import { validateConcreteGraphicProperties } from
|
|
3
|
+
"../../grammar/schemas/concreteGraphic.js";
|
|
4
|
+
|
|
5
|
+
function drawLine(context, child, collectionId) {
|
|
6
|
+
const properties = child.properties ?? {};
|
|
7
|
+
const graphicId = child.id ?? collectionId;
|
|
8
|
+
validateConcreteGraphicProperties("line", properties);
|
|
9
|
+
const x1 = requireFiniteProperty(properties, "x1", graphicId);
|
|
10
|
+
const y1 = requireFiniteProperty(properties, "y1", graphicId);
|
|
11
|
+
const x2 = requireFiniteProperty(properties, "x2", graphicId);
|
|
12
|
+
const y2 = requireFiniteProperty(properties, "y2", graphicId);
|
|
13
|
+
const strokeWidth = requireFiniteProperty(
|
|
14
|
+
properties,
|
|
15
|
+
"strokeWidth",
|
|
16
|
+
graphicId
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
if (strokeWidth < 0) {
|
|
20
|
+
throw new Error(
|
|
21
|
+
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (typeof properties.stroke !== "string") {
|
|
26
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const opacity = properties.opacity ?? 1;
|
|
30
|
+
|
|
31
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
|
|
32
|
+
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
context.strokeStyle = properties.stroke;
|
|
36
|
+
context.lineWidth = strokeWidth;
|
|
37
|
+
context.globalAlpha = opacity;
|
|
38
|
+
context.setLineDash(properties.strokeDash ?? []);
|
|
39
|
+
context.beginPath();
|
|
40
|
+
context.moveTo(x1, y1);
|
|
41
|
+
context.lineTo(x2, y2);
|
|
42
|
+
context.stroke();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function drawLineGraphic(context, id, graphic) {
|
|
46
|
+
if (graphic.items) {
|
|
47
|
+
for (const item of graphic.items) {
|
|
48
|
+
drawLine(context, item, id);
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
drawLine(context, { id, properties: graphic.properties }, id);
|
|
54
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { requireFiniteProperty } from "./validation.js";
|
|
2
|
+
import { validateConcreteGraphicProperties } from
|
|
3
|
+
"../../grammar/schemas/concreteGraphic.js";
|
|
4
|
+
|
|
5
|
+
function drawPath(context, child, collectionId) {
|
|
6
|
+
const properties = child.properties ?? {};
|
|
7
|
+
const graphicId = child.id ?? collectionId;
|
|
8
|
+
validateConcreteGraphicProperties("path", properties);
|
|
9
|
+
const commands = properties.commands;
|
|
10
|
+
|
|
11
|
+
if (!Array.isArray(commands)) {
|
|
12
|
+
throw new Error(
|
|
13
|
+
`Graphic "${graphicId}" requires concrete path commands.`
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const hasFill = properties.fill !== undefined;
|
|
18
|
+
const hasStroke = properties.stroke !== undefined;
|
|
19
|
+
if (!hasFill && !hasStroke) {
|
|
20
|
+
throw new Error(`Graphic "${graphicId}" requires a fill or stroke property.`);
|
|
21
|
+
}
|
|
22
|
+
if (hasFill && typeof properties.fill !== "string") {
|
|
23
|
+
throw new Error(`Graphic "${graphicId}" requires a string fill property.`);
|
|
24
|
+
}
|
|
25
|
+
if (hasFill && commands.at(-1).op !== "Z") {
|
|
26
|
+
throw new Error(`Graphic "${graphicId}" requires a final Z command when filled.`);
|
|
27
|
+
}
|
|
28
|
+
if (hasStroke && typeof properties.stroke !== "string") {
|
|
29
|
+
throw new Error(`Graphic "${graphicId}" requires a string stroke property.`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const strokeWidth = hasStroke
|
|
33
|
+
? requireFiniteProperty(properties, "strokeWidth", graphicId)
|
|
34
|
+
: undefined;
|
|
35
|
+
if (hasStroke && strokeWidth < 0) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Graphic "${graphicId}" requires a non-negative strokeWidth.`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const strokeDash = properties.strokeDash ?? [];
|
|
42
|
+
if (
|
|
43
|
+
!Array.isArray(strokeDash) ||
|
|
44
|
+
!strokeDash.every(value => Number.isFinite(value) && value >= 0)
|
|
45
|
+
) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`Graphic "${graphicId}" requires a non-negative finite strokeDash array.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const opacity = properties.opacity ?? 1;
|
|
52
|
+
if (!Number.isFinite(opacity) || opacity < 0 || opacity > 1) {
|
|
53
|
+
throw new Error(`Graphic "${graphicId}" requires opacity from 0 to 1.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
context.globalAlpha = opacity;
|
|
57
|
+
context.beginPath();
|
|
58
|
+
for (const command of commands) {
|
|
59
|
+
if (command.op === "M") {
|
|
60
|
+
context.moveTo(command.x, command.y);
|
|
61
|
+
} else if (command.op === "L") {
|
|
62
|
+
context.lineTo(command.x, command.y);
|
|
63
|
+
} else if (command.op === "C") {
|
|
64
|
+
context.bezierCurveTo(
|
|
65
|
+
command.x1,
|
|
66
|
+
command.y1,
|
|
67
|
+
command.x2,
|
|
68
|
+
command.y2,
|
|
69
|
+
command.x,
|
|
70
|
+
command.y
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
context.closePath();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (hasFill) {
|
|
78
|
+
context.fillStyle = properties.fill;
|
|
79
|
+
context.fill();
|
|
80
|
+
}
|
|
81
|
+
if (hasStroke) {
|
|
82
|
+
context.strokeStyle = properties.stroke;
|
|
83
|
+
context.lineWidth = strokeWidth;
|
|
84
|
+
context.setLineDash(strokeDash);
|
|
85
|
+
context.stroke();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function drawPathGraphic(context, id, graphic) {
|
|
90
|
+
if (graphic.items) {
|
|
91
|
+
for (const item of graphic.items) {
|
|
92
|
+
drawPath(context, item, id);
|
|
93
|
+
}
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
drawPath(context, { id, properties: graphic.properties }, id);
|
|
98
|
+
}
|