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,321 @@
|
|
|
1
|
+
import { isPlainObject } from "../../core/immutable.js";
|
|
2
|
+
import {
|
|
3
|
+
validateKeys,
|
|
4
|
+
validateNonEmptyString,
|
|
5
|
+
validateNonNegativeFinite,
|
|
6
|
+
validatePositiveFinite
|
|
7
|
+
} from "../../core/validation.js";
|
|
8
|
+
import { resolveGraphicBounds } from "../../layout/canvas.js";
|
|
9
|
+
import { resolveConcreteGraphicBounds } from "../../grammar/schemas/graphicBounds.js";
|
|
10
|
+
import {
|
|
11
|
+
alignedTitleAnchor,
|
|
12
|
+
buildTitleReadingBlock,
|
|
13
|
+
layoutBoundsIntersect,
|
|
14
|
+
resolveTitleComponentBounds,
|
|
15
|
+
unionTitleBounds
|
|
16
|
+
} from "../../layout/title.js";
|
|
17
|
+
import { DEFAULT_COLORS, DEFAULT_FONT_FAMILY } from "../../theme/defaults.js";
|
|
18
|
+
|
|
19
|
+
const OPTIONS = Object.freeze([
|
|
20
|
+
"text", "subtitle", "position", "align", "offset", "gap", "titleStyle",
|
|
21
|
+
"subtitleStyle", "maxWidth", "wrap", "lineHeight"
|
|
22
|
+
]);
|
|
23
|
+
const STYLE_OPTIONS = Object.freeze([
|
|
24
|
+
"color", "fontSize", "fontFamily", "fontWeight"
|
|
25
|
+
]);
|
|
26
|
+
const POSITIONS = Object.freeze(["top", "bottom", "left", "right"]);
|
|
27
|
+
const ALIGNS = Object.freeze(["left", "center", "right"]);
|
|
28
|
+
const WRAPS = Object.freeze(["word", "character"]);
|
|
29
|
+
const DEFAULTS = Object.freeze({
|
|
30
|
+
position: "top",
|
|
31
|
+
align: "left",
|
|
32
|
+
offset: 0,
|
|
33
|
+
gap: 8,
|
|
34
|
+
titleStyle: Object.freeze({
|
|
35
|
+
color: DEFAULT_COLORS.strongText,
|
|
36
|
+
fontSize: 22,
|
|
37
|
+
fontFamily: DEFAULT_FONT_FAMILY,
|
|
38
|
+
fontWeight: 600
|
|
39
|
+
}),
|
|
40
|
+
subtitleStyle: Object.freeze({
|
|
41
|
+
color: DEFAULT_COLORS.mutedText,
|
|
42
|
+
fontSize: 14,
|
|
43
|
+
fontFamily: DEFAULT_FONT_FAMILY,
|
|
44
|
+
fontWeight: "normal"
|
|
45
|
+
})
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export function validateTitleString(value, label) {
|
|
49
|
+
validateNonEmptyString(value, label);
|
|
50
|
+
if (/\r|\n/u.test(value)) {
|
|
51
|
+
throw new Error(`${label} does not accept explicit newlines.`);
|
|
52
|
+
}
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeStyle(value, defaults, label) {
|
|
57
|
+
if (value !== undefined && !isPlainObject(value)) {
|
|
58
|
+
throw new TypeError(`${label} must be a plain object.`);
|
|
59
|
+
}
|
|
60
|
+
const style = { ...defaults, ...(value ?? {}) };
|
|
61
|
+
validateKeys(style, STYLE_OPTIONS, label);
|
|
62
|
+
validateTitleString(style.color, `${label} color`);
|
|
63
|
+
validateTitleString(style.fontFamily, `${label} fontFamily`);
|
|
64
|
+
validatePositiveFinite(style.fontSize, `${label} fontSize`);
|
|
65
|
+
if (!(
|
|
66
|
+
(typeof style.fontWeight === "string" && style.fontWeight.length > 0) ||
|
|
67
|
+
Number.isFinite(style.fontWeight)
|
|
68
|
+
)) {
|
|
69
|
+
throw new TypeError(`${label} fontWeight must be a non-empty string or number.`);
|
|
70
|
+
}
|
|
71
|
+
return style;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function normalizeConfig(args, defaults, { hasSubtitle }) {
|
|
75
|
+
const position = args.position ?? defaults.position;
|
|
76
|
+
const align = args.align ?? defaults.align;
|
|
77
|
+
const offset = args.offset ?? defaults.offset;
|
|
78
|
+
const gap = args.gap ?? defaults.gap;
|
|
79
|
+
const titleStyle = normalizeStyle(
|
|
80
|
+
args.titleStyle,
|
|
81
|
+
defaults.titleStyle,
|
|
82
|
+
"createTitle.titleStyle"
|
|
83
|
+
);
|
|
84
|
+
const subtitleStyle = normalizeStyle(
|
|
85
|
+
args.subtitleStyle,
|
|
86
|
+
defaults.subtitleStyle,
|
|
87
|
+
"createTitle.subtitleStyle"
|
|
88
|
+
);
|
|
89
|
+
const maxWidth = args.maxWidth ?? defaults.maxWidth;
|
|
90
|
+
const wrap = args.wrap ?? defaults.wrap ?? (maxWidth === undefined ? undefined : "word");
|
|
91
|
+
const lineHeight = args.lineHeight ?? defaults.lineHeight;
|
|
92
|
+
if (!POSITIONS.includes(position)) {
|
|
93
|
+
throw new Error(`Unsupported title position "${position}".`);
|
|
94
|
+
}
|
|
95
|
+
if (!ALIGNS.includes(align)) {
|
|
96
|
+
throw new Error(`Unsupported title align "${align}".`);
|
|
97
|
+
}
|
|
98
|
+
if (!Number.isFinite(offset)) {
|
|
99
|
+
throw new TypeError("Chart title offset must be a finite number.");
|
|
100
|
+
}
|
|
101
|
+
validateNonNegativeFinite(gap, "Chart title gap");
|
|
102
|
+
if (maxWidth !== undefined) {
|
|
103
|
+
validatePositiveFinite(maxWidth, "Chart title maxWidth");
|
|
104
|
+
}
|
|
105
|
+
if (wrap !== undefined && !WRAPS.includes(wrap)) {
|
|
106
|
+
throw new Error(`Unsupported title wrap "${wrap}".`);
|
|
107
|
+
}
|
|
108
|
+
if (maxWidth === undefined && (wrap !== undefined || lineHeight !== undefined)) {
|
|
109
|
+
throw new Error("Chart title wrap and lineHeight require maxWidth.");
|
|
110
|
+
}
|
|
111
|
+
if (lineHeight !== undefined && (
|
|
112
|
+
!Number.isFinite(lineHeight) ||
|
|
113
|
+
lineHeight < titleStyle.fontSize ||
|
|
114
|
+
(hasSubtitle && lineHeight < subtitleStyle.fontSize)
|
|
115
|
+
)) {
|
|
116
|
+
throw new RangeError("Chart title lineHeight must cover every visible fontSize.");
|
|
117
|
+
}
|
|
118
|
+
return {
|
|
119
|
+
position,
|
|
120
|
+
align,
|
|
121
|
+
offset,
|
|
122
|
+
gap,
|
|
123
|
+
titleStyle,
|
|
124
|
+
subtitleStyle,
|
|
125
|
+
...(maxWidth === undefined ? {} : { maxWidth, wrap, ...(lineHeight === undefined
|
|
126
|
+
? {}
|
|
127
|
+
: { lineHeight }) })
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function normalizeTitleOptions(args) {
|
|
132
|
+
if (!isPlainObject(args)) {
|
|
133
|
+
throw new TypeError("createTitle options must be a plain object.");
|
|
134
|
+
}
|
|
135
|
+
validateKeys(args, OPTIONS, "createTitle");
|
|
136
|
+
const text = validateTitleString(args.text, "Chart title text");
|
|
137
|
+
const subtitle = args.subtitle === undefined
|
|
138
|
+
? undefined
|
|
139
|
+
: validateTitleString(args.subtitle, "Chart subtitle");
|
|
140
|
+
return {
|
|
141
|
+
text,
|
|
142
|
+
subtitle,
|
|
143
|
+
...normalizeConfig(args, DEFAULTS, { hasSubtitle: subtitle !== undefined })
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function normalizeTitleEditOptions(args, previous, semanticTitle) {
|
|
148
|
+
if (!isPlainObject(args)) {
|
|
149
|
+
throw new TypeError("editTitle options must be a plain object.");
|
|
150
|
+
}
|
|
151
|
+
validateKeys(args, OPTIONS, "editTitle");
|
|
152
|
+
if (Object.keys(args).length === 0) {
|
|
153
|
+
throw new Error("editTitle requires at least one change.");
|
|
154
|
+
}
|
|
155
|
+
const text = args.text === undefined
|
|
156
|
+
? semanticTitle.text
|
|
157
|
+
: validateTitleString(args.text, "Chart title text");
|
|
158
|
+
const subtitle = args.subtitle === false
|
|
159
|
+
? undefined
|
|
160
|
+
: args.subtitle === undefined
|
|
161
|
+
? semanticTitle.subtitle
|
|
162
|
+
: validateTitleString(args.subtitle, "Chart subtitle");
|
|
163
|
+
if (args.subtitle !== undefined && args.subtitle !== false && typeof args.subtitle !== "string") {
|
|
164
|
+
throw new TypeError("Chart subtitle must be a non-empty string or false.");
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
text,
|
|
168
|
+
subtitle,
|
|
169
|
+
config: normalizeConfig(args, previous, { hasSubtitle: subtitle !== undefined })
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function requireTitleConfig(program) {
|
|
174
|
+
if (program.titleConfig === undefined) {
|
|
175
|
+
throw new Error("Title component requires chart title configuration.");
|
|
176
|
+
}
|
|
177
|
+
return program.titleConfig;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function reservedGraphicIds(program, position) {
|
|
181
|
+
const ids = [];
|
|
182
|
+
const axisChannel = ["top", "bottom"].includes(position) ? "x" : "y";
|
|
183
|
+
const axis = program.guideConfigs.axis?.[axisChannel];
|
|
184
|
+
if (axis && Object.values(axis).some(config => config?.position === position)) {
|
|
185
|
+
const prefix = axisChannel === "x" ? "xAxis" : "yAxis";
|
|
186
|
+
ids.push(`${prefix}Line`, `${prefix}Ticks`, `${prefix}Labels`, `${prefix}Title`);
|
|
187
|
+
}
|
|
188
|
+
const prefixes = {
|
|
189
|
+
series: "seriesLegend",
|
|
190
|
+
color: "colorLegend",
|
|
191
|
+
gradient: "colorGradient",
|
|
192
|
+
opacity: "opacityLegend"
|
|
193
|
+
};
|
|
194
|
+
for (const [kind, config] of Object.entries(program.guideConfigs.legend ?? {})) {
|
|
195
|
+
if (config?.position !== position || prefixes[kind] === undefined) continue;
|
|
196
|
+
ids.push(...Object.keys(program.graphicSpec.objects).filter(
|
|
197
|
+
id => id.startsWith(prefixes[kind])
|
|
198
|
+
));
|
|
199
|
+
if (kind === "series" && program.guideConfigs.legend?.size !== undefined) {
|
|
200
|
+
ids.push(...Object.keys(program.graphicSpec.objects).filter(
|
|
201
|
+
id => id.startsWith("sizeLegend")
|
|
202
|
+
));
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return [...new Set(ids)];
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function validateLayout(program, position, titleBounds, plot, canvas) {
|
|
209
|
+
if (
|
|
210
|
+
titleBounds.left < 0 || titleBounds.top < 0 ||
|
|
211
|
+
titleBounds.right > canvas.width || titleBounds.bottom > canvas.height
|
|
212
|
+
) {
|
|
213
|
+
throw new Error(`Chart title requires more ${position}-margin space.`);
|
|
214
|
+
}
|
|
215
|
+
const outsidePlot = {
|
|
216
|
+
top: titleBounds.bottom <= plot.y,
|
|
217
|
+
bottom: titleBounds.top >= plot.y + plot.height,
|
|
218
|
+
left: titleBounds.right <= plot.x,
|
|
219
|
+
right: titleBounds.left >= plot.x + plot.width
|
|
220
|
+
}[position];
|
|
221
|
+
if (!outsidePlot) {
|
|
222
|
+
throw new Error(`Chart title requires more ${position}-margin space.`);
|
|
223
|
+
}
|
|
224
|
+
for (const id of reservedGraphicIds(program, position)) {
|
|
225
|
+
const bounds = program.graphicSpec.objects[id] === undefined
|
|
226
|
+
? undefined
|
|
227
|
+
: resolveConcreteGraphicBounds(program.graphicSpec, id);
|
|
228
|
+
if (bounds !== undefined && layoutBoundsIntersect(titleBounds, bounds)) {
|
|
229
|
+
throw new Error(`Chart title and ${position} guides require more margin space.`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function resolveTitleLayout(program, config) {
|
|
235
|
+
const plot = resolveGraphicBounds(program);
|
|
236
|
+
const canvas = program.graphicSpec.objects.canvas;
|
|
237
|
+
if (
|
|
238
|
+
plot === undefined ||
|
|
239
|
+
![plot.x, plot.y, plot.width, plot.height].every(Number.isFinite) ||
|
|
240
|
+
canvas?.type !== "canvas" ||
|
|
241
|
+
!Number.isFinite(canvas.properties.width) ||
|
|
242
|
+
!Number.isFinite(canvas.properties.height)
|
|
243
|
+
) {
|
|
244
|
+
throw new Error("Chart title layout requires Canvas bounds and dimensions.");
|
|
245
|
+
}
|
|
246
|
+
const block = buildTitleReadingBlock({
|
|
247
|
+
text: validateTitleString(program.semanticSpec.title.text, "Chart title text"),
|
|
248
|
+
subtitle: program.semanticSpec.title.subtitle === undefined
|
|
249
|
+
? undefined
|
|
250
|
+
: validateTitleString(
|
|
251
|
+
program.semanticSpec.title.subtitle,
|
|
252
|
+
"Chart subtitle"
|
|
253
|
+
)
|
|
254
|
+
}, config);
|
|
255
|
+
const horizontal = ["top", "bottom"].includes(config.position);
|
|
256
|
+
let title;
|
|
257
|
+
let subtitle;
|
|
258
|
+
if (horizontal) {
|
|
259
|
+
const x = config.align === "left"
|
|
260
|
+
? plot.x
|
|
261
|
+
: config.align === "center"
|
|
262
|
+
? plot.x + plot.width / 2
|
|
263
|
+
: plot.x + plot.width;
|
|
264
|
+
const blockTop = config.position === "top"
|
|
265
|
+
? 16 + config.offset
|
|
266
|
+
: plot.y + plot.height + config.offset;
|
|
267
|
+
title = {
|
|
268
|
+
lines: block.titleLines,
|
|
269
|
+
x,
|
|
270
|
+
y: block.titleCenters.map(value => blockTop + value),
|
|
271
|
+
textAlign: config.align,
|
|
272
|
+
rotation: 0,
|
|
273
|
+
explicitRotation: config.position !== "top" || block.titleLines.length > 1
|
|
274
|
+
};
|
|
275
|
+
subtitle = block.subtitleLines.length === 0 ? undefined : {
|
|
276
|
+
lines: block.subtitleLines,
|
|
277
|
+
x,
|
|
278
|
+
y: block.subtitleCenters.map(value => blockTop + value),
|
|
279
|
+
textAlign: config.align,
|
|
280
|
+
rotation: 0,
|
|
281
|
+
explicitRotation: config.position !== "top" || block.subtitleLines.length > 1
|
|
282
|
+
};
|
|
283
|
+
} else {
|
|
284
|
+
const rotation = config.position === "left" ? -Math.PI / 2 : Math.PI / 2;
|
|
285
|
+
const y = alignedTitleAnchor(plot.y, plot.height, block.width, config.align);
|
|
286
|
+
const edge = config.position === "left"
|
|
287
|
+
? 16 + config.offset
|
|
288
|
+
: canvas.properties.width - 16 + config.offset;
|
|
289
|
+
const mapX = value => config.position === "left" ? edge + value : edge - value;
|
|
290
|
+
title = {
|
|
291
|
+
lines: block.titleLines,
|
|
292
|
+
x: block.titleCenters.map(mapX),
|
|
293
|
+
y,
|
|
294
|
+
textAlign: "center",
|
|
295
|
+
rotation,
|
|
296
|
+
explicitRotation: true
|
|
297
|
+
};
|
|
298
|
+
subtitle = block.subtitleLines.length === 0 ? undefined : {
|
|
299
|
+
lines: block.subtitleLines,
|
|
300
|
+
x: block.subtitleCenters.map(mapX),
|
|
301
|
+
y,
|
|
302
|
+
textAlign: "center",
|
|
303
|
+
rotation,
|
|
304
|
+
explicitRotation: true
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
const titleBounds = unionTitleBounds([
|
|
308
|
+
resolveTitleComponentBounds(title, config.titleStyle),
|
|
309
|
+
...(subtitle === undefined
|
|
310
|
+
? []
|
|
311
|
+
: [resolveTitleComponentBounds(subtitle, config.subtitleStyle)])
|
|
312
|
+
]);
|
|
313
|
+
validateLayout(
|
|
314
|
+
program,
|
|
315
|
+
config.position,
|
|
316
|
+
titleBounds,
|
|
317
|
+
plot,
|
|
318
|
+
canvas.properties
|
|
319
|
+
);
|
|
320
|
+
return { title, subtitle, bounds: titleBounds };
|
|
321
|
+
}
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import {
|
|
2
|
+
appendActionNodeAtPath,
|
|
3
|
+
countActionNodes,
|
|
4
|
+
createActionNode
|
|
5
|
+
} from "./action.js";
|
|
6
|
+
import {
|
|
7
|
+
cloneAndFreeze,
|
|
8
|
+
freezeOwned,
|
|
9
|
+
isOwned,
|
|
10
|
+
isPlainObject,
|
|
11
|
+
removeOwnedPath
|
|
12
|
+
} from "./immutable.js";
|
|
13
|
+
import {
|
|
14
|
+
createEmptyGraphicSpec,
|
|
15
|
+
createEmptySemanticSpec,
|
|
16
|
+
createTraceRoot
|
|
17
|
+
} from "./specs.js";
|
|
18
|
+
import { LEGEND_CONFIG_KINDS } from "./vocabulary.js";
|
|
19
|
+
|
|
20
|
+
function ownState(value) {
|
|
21
|
+
return isOwned(value) ? value : cloneAndFreeze(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createMaterializationConfigs(
|
|
25
|
+
markConfigs,
|
|
26
|
+
guideConfigs,
|
|
27
|
+
titleConfig,
|
|
28
|
+
canvasConfig
|
|
29
|
+
) {
|
|
30
|
+
return cloneAndFreeze({
|
|
31
|
+
marks: markConfigs,
|
|
32
|
+
guides: guideConfigs,
|
|
33
|
+
...(canvasConfig === undefined ? {} : { canvas: canvasConfig }),
|
|
34
|
+
...(titleConfig === undefined ? {} : { title: titleConfig })
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function updateConfigPath(value, path, config) {
|
|
39
|
+
if (path.length === 0) return cloneAndFreeze(config);
|
|
40
|
+
const [key, ...rest] = path;
|
|
41
|
+
return freezeOwned({
|
|
42
|
+
...value,
|
|
43
|
+
[key]: updateConfigPath(value?.[key] ?? {}, rest, config)
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class ChartProgram {
|
|
48
|
+
constructor({
|
|
49
|
+
semanticSpec = createEmptySemanticSpec(),
|
|
50
|
+
graphicSpec = createEmptyGraphicSpec(),
|
|
51
|
+
resolvedScales = {},
|
|
52
|
+
materializationConfigs,
|
|
53
|
+
markConfigs = {},
|
|
54
|
+
guideConfigs = {},
|
|
55
|
+
titleConfig,
|
|
56
|
+
canvasConfig,
|
|
57
|
+
context = {},
|
|
58
|
+
trace = createTraceRoot(),
|
|
59
|
+
actionStack = [],
|
|
60
|
+
actionSequence
|
|
61
|
+
} = {}) {
|
|
62
|
+
this.semanticSpec = ownState(semanticSpec);
|
|
63
|
+
this.graphicSpec = ownState(graphicSpec);
|
|
64
|
+
this.resolvedScales = ownState(resolvedScales);
|
|
65
|
+
this.materializationConfigs = materializationConfigs === undefined
|
|
66
|
+
? createMaterializationConfigs(
|
|
67
|
+
markConfigs,
|
|
68
|
+
guideConfigs,
|
|
69
|
+
titleConfig,
|
|
70
|
+
canvasConfig
|
|
71
|
+
)
|
|
72
|
+
: ownState(materializationConfigs);
|
|
73
|
+
this.context = ownState(context);
|
|
74
|
+
this.trace = ownState(trace);
|
|
75
|
+
this.actionStack = ownState(actionStack);
|
|
76
|
+
Object.defineProperty(this, "_actionSequence", {
|
|
77
|
+
value: actionSequence ?? countActionNodes(this.trace),
|
|
78
|
+
enumerable: false
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
Object.freeze(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get markConfigs() {
|
|
85
|
+
return this.materializationConfigs.marks;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get guideConfigs() {
|
|
89
|
+
return this.materializationConfigs.guides;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
get titleConfig() {
|
|
93
|
+
return this.materializationConfigs.title;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
_withSelectionConfig(id, config) {
|
|
97
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
98
|
+
throw new TypeError("Selection config id must be a non-empty string.");
|
|
99
|
+
}
|
|
100
|
+
if (!isPlainObject(config)) {
|
|
101
|
+
throw new TypeError("Selection config must be a plain object.");
|
|
102
|
+
}
|
|
103
|
+
return this._withMaterializationConfig(["selections", id], config);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
_withHighlightConfig(id, config) {
|
|
107
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
108
|
+
throw new TypeError("Highlight config id must be a non-empty string.");
|
|
109
|
+
}
|
|
110
|
+
if (!isPlainObject(config)) {
|
|
111
|
+
throw new TypeError("Highlight config must be a plain object.");
|
|
112
|
+
}
|
|
113
|
+
return this._withMaterializationConfig(["highlights", id], config);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
_clone({
|
|
117
|
+
semanticSpec = this.semanticSpec,
|
|
118
|
+
graphicSpec = this.graphicSpec,
|
|
119
|
+
resolvedScales = this.resolvedScales,
|
|
120
|
+
materializationConfigs = this.materializationConfigs,
|
|
121
|
+
context = this.context,
|
|
122
|
+
trace = this.trace,
|
|
123
|
+
actionStack = this.actionStack,
|
|
124
|
+
actionSequence = this._actionSequence
|
|
125
|
+
} = {}) {
|
|
126
|
+
return new this.constructor({
|
|
127
|
+
semanticSpec,
|
|
128
|
+
graphicSpec,
|
|
129
|
+
resolvedScales,
|
|
130
|
+
materializationConfigs,
|
|
131
|
+
context,
|
|
132
|
+
trace,
|
|
133
|
+
actionStack,
|
|
134
|
+
actionSequence
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
_withContext(patch = {}) {
|
|
139
|
+
if (!isPlainObject(patch)) {
|
|
140
|
+
throw new TypeError("Context patch must be a plain object.");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const ownedPatch = cloneAndFreeze(patch);
|
|
144
|
+
|
|
145
|
+
return this._clone({
|
|
146
|
+
context: freezeOwned({
|
|
147
|
+
...this.context,
|
|
148
|
+
...ownedPatch
|
|
149
|
+
})
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_withResolvedScale(id, scale) {
|
|
154
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
155
|
+
throw new TypeError("Resolved scale id must be a non-empty string.");
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isPlainObject(scale)) {
|
|
159
|
+
throw new TypeError("Resolved scale must be a plain object.");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const ownedScale = cloneAndFreeze(scale);
|
|
163
|
+
|
|
164
|
+
return this._clone({
|
|
165
|
+
resolvedScales: freezeOwned({
|
|
166
|
+
...this.resolvedScales,
|
|
167
|
+
[id]: ownedScale
|
|
168
|
+
})
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_withMarkConfig(id, config) {
|
|
173
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
174
|
+
throw new TypeError("Mark config id must be a non-empty string.");
|
|
175
|
+
}
|
|
176
|
+
if (!isPlainObject(config)) {
|
|
177
|
+
throw new TypeError("Mark config must be a plain object.");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return this._withMaterializationConfig(["marks", id], config);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
_withCanvasConfig(config) {
|
|
184
|
+
if (!isPlainObject(config)) {
|
|
185
|
+
throw new TypeError("Canvas config must be a plain object.");
|
|
186
|
+
}
|
|
187
|
+
return this._withMaterializationConfig(["canvas"], config);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
_withMaterializationConfig(path, config) {
|
|
191
|
+
if (
|
|
192
|
+
!Array.isArray(path) ||
|
|
193
|
+
path.length === 0 ||
|
|
194
|
+
!path.every(key => typeof key === "string" && key.length > 0)
|
|
195
|
+
) {
|
|
196
|
+
throw new TypeError("Materialization config path must contain names.");
|
|
197
|
+
}
|
|
198
|
+
if (!isPlainObject(config)) {
|
|
199
|
+
throw new TypeError("Materialization config must be a plain object.");
|
|
200
|
+
}
|
|
201
|
+
return this._clone({
|
|
202
|
+
materializationConfigs: updateConfigPath(
|
|
203
|
+
this.materializationConfigs,
|
|
204
|
+
path,
|
|
205
|
+
config
|
|
206
|
+
)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
_withoutMaterializationConfig(path) {
|
|
211
|
+
if (
|
|
212
|
+
!Array.isArray(path) ||
|
|
213
|
+
path.length === 0 ||
|
|
214
|
+
!path.every(key => typeof key === "string" && key.length > 0)
|
|
215
|
+
) {
|
|
216
|
+
throw new TypeError("Materialization config path must contain names.");
|
|
217
|
+
}
|
|
218
|
+
const removed = removeOwnedPath(this.materializationConfigs, path);
|
|
219
|
+
if (!removed.removed) return this;
|
|
220
|
+
return this._clone({
|
|
221
|
+
materializationConfigs: freezeOwned({
|
|
222
|
+
...removed.value,
|
|
223
|
+
marks: removed.value.marks ?? freezeOwned({}),
|
|
224
|
+
guides: removed.value.guides ?? freezeOwned({})
|
|
225
|
+
})
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
_withGuideConfig(channel, component, config) {
|
|
230
|
+
if (config === undefined) {
|
|
231
|
+
config = component;
|
|
232
|
+
component = "ticks";
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return this._withMaterializationConfig(
|
|
236
|
+
["guides", "axis", channel, component],
|
|
237
|
+
config
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
_withLegendConfig(kind, config) {
|
|
242
|
+
if (config === undefined) {
|
|
243
|
+
config = kind;
|
|
244
|
+
kind = "series";
|
|
245
|
+
}
|
|
246
|
+
if (!LEGEND_CONFIG_KINDS.includes(kind)) {
|
|
247
|
+
throw new Error(`Unknown legend kind "${kind}".`);
|
|
248
|
+
}
|
|
249
|
+
if (!isPlainObject(config)) {
|
|
250
|
+
throw new TypeError("Legend config must be a plain object.");
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return this._withMaterializationConfig(
|
|
254
|
+
["guides", "legend", kind],
|
|
255
|
+
config
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
_withGridConfig(direction, config) {
|
|
260
|
+
if (!["horizontal", "vertical"].includes(direction)) {
|
|
261
|
+
throw new Error(`Unknown grid direction "${direction}".`);
|
|
262
|
+
}
|
|
263
|
+
if (!isPlainObject(config)) {
|
|
264
|
+
throw new TypeError("Grid config must be a plain object.");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return this._withMaterializationConfig(
|
|
268
|
+
["guides", "grid", direction],
|
|
269
|
+
config
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
_withTitleConfig(config) {
|
|
274
|
+
if (!isPlainObject(config)) {
|
|
275
|
+
throw new TypeError("Title config must be a plain object.");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return this._withMaterializationConfig(["title"], config);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
_enterAction({ op, description, args }) {
|
|
282
|
+
const actionSequence = this._actionSequence + 1;
|
|
283
|
+
const id = `a${actionSequence}`;
|
|
284
|
+
const parentPath = this.actionStack.at(-1)?.path ?? [];
|
|
285
|
+
const actionNode = createActionNode({ id, op, description, args });
|
|
286
|
+
const appended = appendActionNodeAtPath(
|
|
287
|
+
this.trace,
|
|
288
|
+
parentPath,
|
|
289
|
+
actionNode
|
|
290
|
+
);
|
|
291
|
+
const actionStack = cloneAndFreeze([
|
|
292
|
+
...this.actionStack,
|
|
293
|
+
{ id, path: appended.path }
|
|
294
|
+
]);
|
|
295
|
+
|
|
296
|
+
return this._clone({
|
|
297
|
+
trace: appended.root,
|
|
298
|
+
actionStack,
|
|
299
|
+
actionSequence
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
_exitAction() {
|
|
304
|
+
if (this.actionStack.length === 0) {
|
|
305
|
+
throw new Error("Cannot exit an action when the action stack is empty.");
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
return this._clone({
|
|
309
|
+
actionStack: cloneAndFreeze(this.actionStack.slice(0, -1))
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function chart() {
|
|
315
|
+
return new ChartProgram();
|
|
316
|
+
}
|