ggaction 0.0.22 → 0.0.24
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 +35 -0
- package/README.md +82 -90
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/package.json +6 -3
- package/src/actions/charts/heatmap.js +1 -1
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/data/basicBin2d.js +1 -1
- package/src/actions/data/bin2d.js +2 -2
- package/src/actions/data/edit.js +1 -1
- package/src/actions/data/timeUnit.js +1 -1
- package/src/actions/encodings/ranged.js +2 -2
- package/src/actions/facets/actions.js +1 -1
- package/src/actions/guides/axes/components.js +5 -0
- package/src/actions/guides/axes/edit.js +1 -188
- package/src/actions/guides/axes/labels.js +1 -537
- package/src/actions/guides/axes/tickGroups.js +16 -1
- package/src/actions/guides/axes/ticks.js +33 -24
- package/src/actions/guides/axes/titles.js +1 -389
- package/src/actions/guides/legends/categorical/components.js +1 -4
- package/src/actions/regression/create.js +6 -1
- package/src/actions/regression/edit.js +10 -1
- package/src/actions/titles/resolve.js +11 -1
- package/src/core/compositionState.js +9 -4
- package/src/core/facetAxisPolicy.js +30 -0
- package/src/core/facetGap.js +44 -0
- package/src/grammar/axisTextPlacement.js +1 -1
- package/src/grammar/bin2d.js +34 -4
- package/src/grammar/curveCommands.js +35 -2
- package/src/grammar/datasetSchema.js +1 -1
- package/src/grammar/facetHeaders.js +1 -1
- package/src/grammar/facets/guides.js +7 -5
- package/src/grammar/regression/parameters.js +7 -0
- package/src/grammar/timeUnit.js +9 -3
- package/src/inspectionDescriptors.js +1 -1
- package/src/layout/composition.js +2 -6
- package/src/layout/facetContentFlow.js +15 -7
- package/src/layout/facets.js +1 -1
- package/src/layout/guideCollisions.js +9 -3
- package/src/layout/title.js +1 -1
- package/src/materialization/facetContentBounds.js +96 -0
- package/src/materialization/facetGuides/layout.js +1 -1
- package/src/materialization/facetGuides/placement.js +16 -0
- package/src/materialization/facetHeaders.js +1 -1
- package/src/materialization/facets.js +1 -1
- package/src/materialization/graphicHierarchy.js +25 -1
- package/src/materialization/guides/layout.js +1 -1
- package/src/materialization/guides/resources.js +1 -1
- package/src/materialization/planner.js +19 -4
- package/src/persistence/validation.js +25 -1
- package/src/version.js +1 -1
- package/types/basic.d.ts +2 -0
- package/types/index.d.ts +3 -0
- package/types/program.d.ts +36 -18
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { normalizeFacetGap } from "./facetGap.js";
|
|
2
|
+
import { normalizeFacetAxes } from "./facetAxisPolicy.js";
|
|
1
3
|
import { isOwned, isPlainObject } from "./immutable.js";
|
|
2
4
|
import { ownProgramState } from "./programState.js";
|
|
3
5
|
import {
|
|
@@ -43,6 +45,7 @@ function validateFacetSpec(compositionSpec) {
|
|
|
43
45
|
}
|
|
44
46
|
const grid = compositionSpec.facet.grid;
|
|
45
47
|
const repeat = compositionSpec.facet.repeat;
|
|
48
|
+
if (grid !== undefined && compositionSpec.spacing === "content") throw new Error("Content flow requires a wrapped facet.");
|
|
46
49
|
if (grid !== undefined && repeat !== undefined) {
|
|
47
50
|
throw new Error("Facet compositionSpec cannot combine grid and repeat recipes.");
|
|
48
51
|
}
|
|
@@ -204,10 +207,10 @@ function validateFacetSpec(compositionSpec) {
|
|
|
204
207
|
);
|
|
205
208
|
}
|
|
206
209
|
const guides = compositionSpec.facet.guides;
|
|
210
|
+
normalizeFacetAxes(guides?.axes, "each", { stored: true });
|
|
207
211
|
if (
|
|
208
212
|
!isPlainObject(guides) ||
|
|
209
213
|
Object.keys(guides).some(key => !["axes", "legend"].includes(key)) ||
|
|
210
|
-
!["each", "outer"].includes(guides.axes) ||
|
|
211
214
|
(![false, "shared"].includes(guides.legend) && !isPlainObject(guides.legend))
|
|
212
215
|
) {
|
|
213
216
|
throw new Error(
|
|
@@ -240,7 +243,9 @@ function validateCompositionChildren(compositionSpec, childIds, facet) {
|
|
|
240
243
|
}
|
|
241
244
|
|
|
242
245
|
function validateLayout(compositionSpec) {
|
|
243
|
-
if (
|
|
246
|
+
if (compositionSpec.type === "facet") {
|
|
247
|
+
normalizeFacetGap(compositionSpec.gap, "compositionSpec.gap");
|
|
248
|
+
} else if (!Number.isFinite(compositionSpec.gap) || compositionSpec.gap < 0) {
|
|
244
249
|
throw new RangeError("compositionSpec.gap must be a non-negative finite number.");
|
|
245
250
|
}
|
|
246
251
|
if (!["start", "center", "end"].includes(compositionSpec.align)) {
|
|
@@ -289,8 +294,8 @@ export function ownCompositionSpec(compositionSpec, children) {
|
|
|
289
294
|
throw new TypeError("compositionSpec.id must be a non-empty string.");
|
|
290
295
|
}
|
|
291
296
|
const facet = compositionSpec.type === "facet";
|
|
292
|
-
if (compositionSpec.spacing !== undefined && (!facet || !["plot", "canvas"].includes(compositionSpec.spacing))) {
|
|
293
|
-
throw new Error("compositionSpec.spacing requires a facet and canvas or
|
|
297
|
+
if (compositionSpec.spacing !== undefined && (!facet || !["plot", "canvas", "content"].includes(compositionSpec.spacing))) {
|
|
298
|
+
throw new Error("compositionSpec.spacing requires a facet and canvas, plot, or content spacing.");
|
|
294
299
|
}
|
|
295
300
|
if (compositionSpec.topLane !== undefined && (!facet ||
|
|
296
301
|
!FACET_TOP_LANE_MODES.includes(compositionSpec.topLane) ||
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "./immutable.js";
|
|
2
|
+
|
|
3
|
+
export function normalizeFacetAxes(value, previous = "each", { stored = false } = {}) {
|
|
4
|
+
if (value === undefined && !stored) value = "each";
|
|
5
|
+
if (["each", "outer"].includes(value)) return value;
|
|
6
|
+
if (!isPlainObject(value) || Object.keys(value).length === 0 ||
|
|
7
|
+
Object.keys(value).some(key => !["x", "y"].includes(key)) ||
|
|
8
|
+
Object.values(value).some(mode => !["each", "outer"].includes(mode)) ||
|
|
9
|
+
stored && (!Object.hasOwn(value, "x") || !Object.hasOwn(value, "y"))) {
|
|
10
|
+
throw new Error('Facet axes must be each, outer, or a non-empty x/y policy object.');
|
|
11
|
+
}
|
|
12
|
+
return cloneAndFreeze({ x: value.x ?? facetAxisMode(previous, "x"), y: value.y ?? facetAxisMode(previous, "y") });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function facetAxisMode(policy, channel) {
|
|
16
|
+
return typeof policy === "string" ? policy : policy[channel];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function hasOuterFacetAxes(policy) {
|
|
20
|
+
return ["x", "y"].some(channel => facetAxisMode(policy, channel) === "outer");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function validateFacetAxisPolicy(policy, family, repeat = false) {
|
|
24
|
+
if (family !== "cartesian" && (hasOuterFacetAxes(policy) || isPlainObject(policy))) {
|
|
25
|
+
throw new Error("Polar and Parallel facets do not support outer axes or per-channel axis policies.");
|
|
26
|
+
}
|
|
27
|
+
if (repeat && hasOuterFacetAxes(policy)) {
|
|
28
|
+
throw new Error("repeatCharts does not promote axes across different repeated fields.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { cloneAndFreeze, isPlainObject } from "./immutable.js";
|
|
2
|
+
import { annotateError } from "./diagnostics.js";
|
|
3
|
+
export function validateCompositionSpacing(value, label) {
|
|
4
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
5
|
+
throw new RangeError(`${label} must be a non-negative finite number.`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Keep scalar intent intact; a facet pair always declares both axes. */
|
|
11
|
+
export function normalizeFacetGap(value, label = "Facet gap") {
|
|
12
|
+
if (!isPlainObject(value)) return validateCompositionSpacing(value, label);
|
|
13
|
+
const keys = Reflect.ownKeys(value);
|
|
14
|
+
if (keys.length !== 2 || !keys.includes("x") || !keys.includes("y")) {
|
|
15
|
+
throw annotateError(new TypeError(`${label} must contain exactly x and y.`), {
|
|
16
|
+
code: "invalid-value", optionPath: "gap", actual: value
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const axis of ["x", "y"]) {
|
|
21
|
+
try {
|
|
22
|
+
result[axis] = validateCompositionSpacing(value[axis], `${label}.${axis}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw annotateError(error, {
|
|
25
|
+
code: "invalid-value", optionPath: `gap.${axis}`, actual: value[axis], limit: 0
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return cloneAndFreeze(result);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Accept only a previously normalized scalar or pair. */
|
|
33
|
+
export function facetGapAxis(gap, axis) {
|
|
34
|
+
return typeof gap === "number" ? gap : gap[axis];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Overflow remains axis-specific for a pair; scalar callers keep their old error. */
|
|
38
|
+
export function assertFacetGapFinite(gap, axis, values, message) {
|
|
39
|
+
if (values.every(Number.isFinite)) return;
|
|
40
|
+
const error = new RangeError(message);
|
|
41
|
+
throw typeof gap === "number" ? error : annotateError(error, {
|
|
42
|
+
code: "invalid-value", optionPath: `gap.${axis}`, actual: gap[axis]
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{resolveTextLines as
|
|
1
|
+
import{validateKeys as a,validateNonNegativeFinite as o}from"../core/validation.js";import{isPlainObject as s}from"../core/immutable.js";import{resolveTextLines as b}from"../core/textMetrics.js";function c(e,{title:t=!1}={}){if(Object.hasOwn(e,"align")&&!["left","center","right"].includes(e.align))throw new TypeError("Axis text align must be left, center, or right.");if(t){if(Object.hasOwn(e,"overlap")&&!["error","allow"].includes(e.overlap))throw new TypeError("Axis title overlap must be error or allow.");if(Object.hasOwn(e,"baseline")&&!["top","hanging","middle","alphabetic","ideographic","bottom"].includes(e.baseline))throw new TypeError("Axis title baseline must be a supported text baseline.");if(Object.hasOwn(e,"offset")&&s(e.offset)){if(a(e.offset,["from","gap"],"Axis title offset"),e.offset.from!=="labels"||!Object.hasOwn(e.offset,"gap"))throw new TypeError("Axis title offset requires from: labels and gap.");o(e.offset.gap,"Axis title label gap")}for(const i of["offset","alongOffset"])if(!(i==="offset"&&f(e.offset))&&Object.hasOwn(e,i)&&!Number.isFinite(e[i]))throw new TypeError(`Axis title ${i} must be a finite number.`)}}function f(e){return s(e)&&e.from==="labels"}function h(e){return e?.inferredOffset===!0||f(e?.offset)}function m(e){return(e?.lines??[{text:e?.text}]).some(t=>typeof t.text=="string"&&t.text.length>0)}function w(e,t){if(!/[\r\n]/u.test(e))return;const i=t.baseline??"middle",r=b(e,{fontSize:t.fontSize,align:t.align??"center",baseline:i,...i==="middle"?{blockAlign:"middle"}:{}});if(["bottom","ideographic"].includes(i)){const l=(r.length-1)*t.fontSize*1.2;return r.map(n=>({...n,y:n.y-l}))}return r.length>1?r:void 0}function u(e){return e?.overlap==="allow"}function g(e,{allowReset:t=!1}={}){e===void 0||t&&e==="auto"||o(e,"Label flushThreshold")}export{m as axisLabelHasText,u as axisTitleAllowsLabelOverlap,h as axisTitleFollowsLabels,w as axisTitleTextLines,f as isLabelRelativeTitleOffset,g as validateAxisLabelFlushThreshold,c as validateAxisTextPlacement};
|
package/src/grammar/bin2d.js
CHANGED
|
@@ -11,7 +11,7 @@ const MAX_CELLS = 1_000_000;
|
|
|
11
11
|
|
|
12
12
|
const TRANSFORM_KEYS = Object.freeze([
|
|
13
13
|
"type", "x", "y", "bins", "extent", "includeEmpty", "members", "as",
|
|
14
|
-
"resolved"
|
|
14
|
+
"resolved", "assignment"
|
|
15
15
|
]);
|
|
16
16
|
const AXIS_KEYS = Object.freeze(["x", "y"]);
|
|
17
17
|
const FIELD_KEYS = Object.freeze(["x0", "x1", "y0", "y1", "count", "members"]);
|
|
@@ -180,6 +180,11 @@ export function validateBin2DTransform(transform) {
|
|
|
180
180
|
}
|
|
181
181
|
requireField(transform.x, "2D bin x field");
|
|
182
182
|
requireField(transform.y, "2D bin y field");
|
|
183
|
+
if (transform.assignment !== undefined && transform.assignment !== "step") {
|
|
184
|
+
throw new TypeError(
|
|
185
|
+
'Stored 2D bin assignment must be "step" or omitted.'
|
|
186
|
+
);
|
|
187
|
+
}
|
|
183
188
|
validateBins(transform.bins);
|
|
184
189
|
validateRequestedExtent(transform.extent);
|
|
185
190
|
if (typeof transform.includeEmpty !== "boolean") {
|
|
@@ -201,10 +206,16 @@ export function normalizeBin2DTransform({
|
|
|
201
206
|
y,
|
|
202
207
|
bins,
|
|
203
208
|
extent,
|
|
209
|
+
assignment = "edges",
|
|
204
210
|
includeEmpty = false,
|
|
205
211
|
members = false,
|
|
206
212
|
as
|
|
207
213
|
} = {}) {
|
|
214
|
+
if (assignment !== "edges" && assignment !== "step") {
|
|
215
|
+
throw new TypeError(
|
|
216
|
+
'2D bin assignment must be "edges" or "step".'
|
|
217
|
+
);
|
|
218
|
+
}
|
|
208
219
|
const normalizedBins = normalizeBins(bins);
|
|
209
220
|
const transform = {
|
|
210
221
|
type: "bin2d",
|
|
@@ -214,7 +225,8 @@ export function normalizeBin2DTransform({
|
|
|
214
225
|
extent: normalizeRequestedExtent(extent),
|
|
215
226
|
includeEmpty,
|
|
216
227
|
members,
|
|
217
|
-
as: normalizeOutputFields(id, as, members)
|
|
228
|
+
as: normalizeOutputFields(id, as, members),
|
|
229
|
+
...(assignment === "step" ? { assignment } : {})
|
|
218
230
|
};
|
|
219
231
|
validateBin2DTransform(transform);
|
|
220
232
|
return cloneAndFreeze(transform);
|
|
@@ -266,6 +278,20 @@ function findBinIndex(value, edges) {
|
|
|
266
278
|
return low;
|
|
267
279
|
}
|
|
268
280
|
|
|
281
|
+
function findStepBinIndex(value, extent, count) {
|
|
282
|
+
if (count === 1 || value === extent[1]) return count - 1;
|
|
283
|
+
const [minimum, maximum] = extent;
|
|
284
|
+
const span = maximum - minimum;
|
|
285
|
+
const step = Number.isFinite(span)
|
|
286
|
+
? span / count
|
|
287
|
+
: maximum / count - minimum / count;
|
|
288
|
+
const distance = value - minimum;
|
|
289
|
+
const position = Number.isFinite(distance)
|
|
290
|
+
? distance / step
|
|
291
|
+
: value / step - minimum / step;
|
|
292
|
+
return Math.max(0, Math.min(count - 1, Math.floor(1e-14 + position)));
|
|
293
|
+
}
|
|
294
|
+
|
|
269
295
|
export function deriveBin2DRows(rows, transform) {
|
|
270
296
|
if (!Array.isArray(rows)) {
|
|
271
297
|
throw new TypeError("2D bin source rows must be an array.");
|
|
@@ -304,8 +330,12 @@ export function deriveBin2DRows(rows, transform) {
|
|
|
304
330
|
() => []
|
|
305
331
|
);
|
|
306
332
|
for (const item of eligible) {
|
|
307
|
-
const xIndex =
|
|
308
|
-
|
|
333
|
+
const xIndex = transform.assignment === "step"
|
|
334
|
+
? findStepBinIndex(item.row[transform.x], extent.x, transform.bins.x)
|
|
335
|
+
: findBinIndex(item.row[transform.x], edges.x);
|
|
336
|
+
const yIndex = transform.assignment === "step"
|
|
337
|
+
? findStepBinIndex(item.row[transform.y], extent.y, transform.bins.y)
|
|
338
|
+
: findBinIndex(item.row[transform.y], edges.y);
|
|
309
339
|
buckets[yIndex * transform.bins.x + xIndex].push(item.index);
|
|
310
340
|
}
|
|
311
341
|
const values = [];
|
|
@@ -9,6 +9,7 @@ export const CURVE_INTERPOLATIONS = Object.freeze([
|
|
|
9
9
|
"basis",
|
|
10
10
|
"cardinal",
|
|
11
11
|
"monotone",
|
|
12
|
+
"monotone-steffen",
|
|
12
13
|
"natural"
|
|
13
14
|
]);
|
|
14
15
|
|
|
@@ -16,6 +17,7 @@ const SMOOTH_CURVES = new Set([
|
|
|
16
17
|
"basis",
|
|
17
18
|
"cardinal",
|
|
18
19
|
"monotone",
|
|
20
|
+
"monotone-steffen",
|
|
19
21
|
"natural"
|
|
20
22
|
]);
|
|
21
23
|
|
|
@@ -196,8 +198,38 @@ function monotoneTangents(points) {
|
|
|
196
198
|
return tangents;
|
|
197
199
|
}
|
|
198
200
|
|
|
199
|
-
function
|
|
200
|
-
const
|
|
201
|
+
function steffenTangents(points) {
|
|
202
|
+
const widths = points.slice(1).map((point, index) => point.x - points[index].x);
|
|
203
|
+
const direction = Math.sign(widths.find(width => width !== 0) ?? 0);
|
|
204
|
+
if (widths.some(width => width !== 0 && Math.sign(width) !== direction)) {
|
|
205
|
+
throw new Error("Steffen curve requires monotonic x values.");
|
|
206
|
+
}
|
|
207
|
+
const slopes = widths.map((width, i) =>
|
|
208
|
+
(points[i + 1].y - points[i].y) / (width || (direction < 0 ? -0 : 0)));
|
|
209
|
+
const tangents = Array(points.length);
|
|
210
|
+
for (let i = 1; i < points.length - 1; i += 1) {
|
|
211
|
+
const a = slopes[i - 1], b = slopes[i];
|
|
212
|
+
const mean = (a * widths[i] + b * widths[i - 1]) / (widths[i - 1] + widths[i]);
|
|
213
|
+
// A zero-width turn has no finite secant estimate; its Hermite limit is flat.
|
|
214
|
+
const sign = value => value < 0 ? -1 : 1;
|
|
215
|
+
const slope = (sign(a) + sign(b)) * Math.min(Math.abs(a), Math.abs(b), Math.abs(mean) / 2);
|
|
216
|
+
tangents[i] = Number.isNaN(slope) && (widths[i - 1] === 0 || widths[i] === 0) ? 0 : slope;
|
|
217
|
+
}
|
|
218
|
+
tangents[0] = widths[0] === 0 ? tangents[1] : (3 * slopes[0] - tangents[1]) / 2;
|
|
219
|
+
tangents[points.length - 1] = widths.at(-1) === 0 ? tangents.at(-2) :
|
|
220
|
+
(3 * slopes.at(-1) - tangents.at(-2)) / 2;
|
|
221
|
+
return tangents;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function buildSteffenCommands(points) {
|
|
225
|
+
const distinct = points.filter((point, i) => i === 0 ||
|
|
226
|
+
point.x !== points[i - 1].x || point.y !== points[i - 1].y);
|
|
227
|
+
if (distinct.length === 1) return ownCommands([{ op: "M", ...distinct[0] }]);
|
|
228
|
+
if (distinct.length === 2) return buildLinearPathCommands(distinct);
|
|
229
|
+
return buildMonotoneCommands(distinct, steffenTangents(distinct));
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function buildMonotoneCommands(points, tangents = monotoneTangents(points)) {
|
|
201
233
|
const commands = [{ op: "M", x: points[0].x, y: points[0].y }];
|
|
202
234
|
for (let index = 1; index < points.length; index += 1) {
|
|
203
235
|
const previous = points[index - 1];
|
|
@@ -274,6 +306,7 @@ function buildNaturalCommands(points) {
|
|
|
274
306
|
}
|
|
275
307
|
|
|
276
308
|
function buildValidatedCurvePathCommands(points, curve, tension) {
|
|
309
|
+
if (curve === "monotone-steffen") return buildSteffenCommands(points);
|
|
277
310
|
if (curve === "linear" ||
|
|
278
311
|
(SMOOTH_CURVES.has(curve) &&
|
|
279
312
|
curve !== "monotone" &&
|
|
@@ -404,7 +404,7 @@ export function deriveTransformSchema(inputSchema, transform, values, { sourceId
|
|
|
404
404
|
if (planned.lineage === undefined && !["complete", "impute"].includes(transform.type)) {
|
|
405
405
|
return planned;
|
|
406
406
|
}
|
|
407
|
-
return { ...planned, storageType: transform.type
|
|
407
|
+
return { ...planned, storageType: ["regressionStatistics", "timeUnit"].includes(transform.type) && planned.storageType === "number"
|
|
408
408
|
? "number" : field.storageType, nullable: field.nullable, optional: field.optional };
|
|
409
409
|
});
|
|
410
410
|
for (const field of inferred.fields) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\r\n\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title text must be one line.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot or
|
|
1
|
+
import{validateOptionObject as o,validateNonEmptyString as r,validatePositiveFinite as a,validateNonNegativeFinite as c}from"../core/validation.js";import{resolveRotation as s}from"./rotation.js";const f=Object.freeze(["text","fontSize","fontFamily","fontWeight","color","offset","align","rotation","alongOffset"]);function d(e){if(o(e,f,"Facet header title",{allowEmpty:!1}),r(e.text,"Facet header title text"),/[\r\n\u2028\u2029]/u.test(e.text))throw new TypeError("Facet header title text must be one line.");e.fontSize!==void 0&&a(e.fontSize,"Facet header title fontSize");for(const t of["fontFamily","color"])e[t]!==void 0&&r(e[t],`Facet header title ${t}`);if(e.fontWeight!==void 0&&!(typeof e.fontWeight=="string"&&e.fontWeight.length>0||Number.isFinite(e.fontWeight)))throw new TypeError("Facet header title fontWeight must be a non-empty string or finite number.");if(e.offset!==void 0&&c(e.offset,"Facet header title offset"),e.align!==void 0&&!["start","center","end"].includes(e.align))throw new TypeError("Facet header title align must be start, center, or end.");for(const t of["rotation","alongOffset"])if(Object.hasOwn(e,t)&&!Number.isFinite(e[t]))throw new TypeError(`Facet header title ${t} must contain finite normalized radians or coordinates.`)}function h(e,t){if(t===!1)return;o(t,f,"Facet header title",{allowEmpty:!1});for(const i of Object.keys(t))if(t[i]===void 0)throw new TypeError(`Facet header title ${i} must not be undefined.`);const n={...e,...t};return Object.hasOwn(t,"rotation")&&(n.rotation=s(t.rotation,"Facet header title rotation")),d(n),n}function w(e){for(const t of e.common===void 0?[e]:[e.common,e.row,e.column]){if(t!==void 0&&Object.hasOwn(t,"anchor")&&!["plot","content","view"].includes(t.anchor))throw new TypeError("Facet header anchor must be plot, content, or view.");if(t?.rotation!==void 0&&!Number.isFinite(t.rotation))throw new TypeError("Facet header rotation must contain finite normalized radians.");t?.title!==void 0&&d(t.title)}if(e.title!==void 0||e.common?.title!==void 0)throw new TypeError("Facet header title requires an explicit row or column role.");if(e.mode!=="roles"&&(e.row?.title!==void 0||e.column?.title!==void 0))throw new TypeError("Facet field titles require role header mode.")}export{h as normalizeFacetHeaderTitle,w as validateFacetHeaderRotations,d as validateFacetHeaderTitle};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { cloneAndFreeze, isPlainObject } from "../../core/immutable.js";
|
|
2
2
|
|
|
3
|
+
|
|
4
|
+
import { normalizeFacetAxes, facetAxisMode, hasOuterFacetAxes } from "../../core/facetAxisPolicy.js";
|
|
5
|
+
|
|
3
6
|
const LEGEND_FAMILIES = Object.freeze({
|
|
4
7
|
series: "categorical",
|
|
5
8
|
color: "categorical",
|
|
@@ -243,9 +246,7 @@ export function planFacetGuideOwnership({
|
|
|
243
246
|
if (!Array.isArray(children) || children.length !== cells.length) {
|
|
244
247
|
throw new Error("Facet guide ownership requires one child descriptor per placement.");
|
|
245
248
|
}
|
|
246
|
-
|
|
247
|
-
throw new Error("Facet axes must be each or outer.");
|
|
248
|
-
}
|
|
249
|
+
axes = normalizeFacetAxes(axes, "each", { stored: true });
|
|
249
250
|
if (![false, "shared"].includes(legend) && !isPlainObject(legend)) {
|
|
250
251
|
throw new Error("Facet legend must be false or shared.");
|
|
251
252
|
}
|
|
@@ -264,11 +265,12 @@ export function planFacetGuideOwnership({
|
|
|
264
265
|
const presentAxes = descriptor.axes ?? [];
|
|
265
266
|
const records = descriptor.axisOwners;
|
|
266
267
|
const removed = {};
|
|
267
|
-
const keepAxes = axes
|
|
268
|
-
? presentAxes.filter(channel => outer.children[cell.id][channel])
|
|
268
|
+
const keepAxes = !hasOuterFacetAxes(axes) ? presentAxes : records === undefined
|
|
269
|
+
? presentAxes.filter(channel => facetAxisMode(axes, channel) === "each" || outer.children[cell.id][channel])
|
|
269
270
|
: presentAxes.filter(id => {
|
|
270
271
|
const owner = records.find(owner => owner.id === id);
|
|
271
272
|
if (owner === undefined) throw new Error("Facet axis descriptors must identify every retained owner.");
|
|
273
|
+
if (facetAxisMode(axes, owner.channel) === "each") return true;
|
|
272
274
|
const components = Object.entries(owner.components);
|
|
273
275
|
const remove = components.filter(([, side]) => !sides[side]?.includes(cell.id)).map(([component]) => component);
|
|
274
276
|
if (remove.length > 0 && remove.length < components.length) removed[id] = remove;
|
|
@@ -233,3 +233,10 @@ export function normalizeRegressionTransform(args = {}) {
|
|
|
233
233
|
validateRegressionTransform(transform);
|
|
234
234
|
return cloneAndFreeze(transform);
|
|
235
235
|
}
|
|
236
|
+
|
|
237
|
+
export function validateRegressionPlacement(value) {
|
|
238
|
+
if (value !== undefined && !["source", "plot-last"].includes(value)) {
|
|
239
|
+
throw new Error('Regression placement must be "source" or "plot-last".');
|
|
240
|
+
}
|
|
241
|
+
return value ?? "source";
|
|
242
|
+
}
|
package/src/grammar/timeUnit.js
CHANGED
|
@@ -41,7 +41,7 @@ export const TIME_UNITS = cloneAndFreeze([
|
|
|
41
41
|
|
|
42
42
|
const TRANSFORM_KEYS = Object.freeze([
|
|
43
43
|
"type", "field", "unit", "as", "temporalUnit", "timeZone",
|
|
44
|
-
"weekStartsOn", "weekRule"
|
|
44
|
+
"weekStartsOn", "weekRule", "missing"
|
|
45
45
|
]);
|
|
46
46
|
|
|
47
47
|
const CALENDAR_FORMATTERS = new Map();
|
|
@@ -93,6 +93,9 @@ export function validateTimeUnitTransform(transform) {
|
|
|
93
93
|
if (transform.type !== "timeUnit") {
|
|
94
94
|
throw new Error(`Unsupported time-unit transform "${transform.type}".`);
|
|
95
95
|
}
|
|
96
|
+
if (Object.hasOwn(transform, "missing") && !["error", "null"].includes(transform.missing)) {
|
|
97
|
+
throw new Error('Time-unit missing must be "error" or "null".');
|
|
98
|
+
}
|
|
96
99
|
validateTemporalUnit(transform.temporalUnit);
|
|
97
100
|
const field = requireField(transform.field, "Time-unit field");
|
|
98
101
|
const output = requireField(transform.as, "Time-unit output field");
|
|
@@ -127,7 +130,8 @@ export function normalizeTimeUnitTransform(args = {}) {
|
|
|
127
130
|
const as = args.as === undefined ? `${field}_${unit}` : args.as;
|
|
128
131
|
const transform = { type: "timeUnit", field, unit, as,
|
|
129
132
|
...(temporalUnit === undefined ? {} : { temporalUnit }),
|
|
130
|
-
...(timeZone === undefined ? {} : { timeZone })
|
|
133
|
+
...(timeZone === undefined ? {} : { timeZone }),
|
|
134
|
+
...(Object.hasOwn(args, "missing") ? { missing: args.missing } : {}) };
|
|
131
135
|
if (unit === "week") {
|
|
132
136
|
transform.weekStartsOn = args.weekStartsOn ?? 1;
|
|
133
137
|
transform.weekRule = args.weekRule ?? "calendar";
|
|
@@ -319,7 +323,8 @@ export function deriveTimeUnitRows(rows, transform) {
|
|
|
319
323
|
throw new TypeError("Time-unit source must contain plain row objects.");
|
|
320
324
|
}
|
|
321
325
|
const values = rows.map((row, index) => {
|
|
322
|
-
|
|
326
|
+
const missing = !Object.hasOwn(row, transform.field) || row[transform.field] === null;
|
|
327
|
+
if (!Object.hasOwn(row, transform.field) && transform.missing !== "null") {
|
|
323
328
|
throw new Error(
|
|
324
329
|
`Time-unit source does not contain field "${transform.field}" at row ${index}.`
|
|
325
330
|
);
|
|
@@ -327,6 +332,7 @@ export function deriveTimeUnitRows(rows, transform) {
|
|
|
327
332
|
if (Object.hasOwn(row, transform.as)) {
|
|
328
333
|
throw new Error(`Time-unit output field "${transform.as}" already exists.`);
|
|
329
334
|
}
|
|
335
|
+
if (missing && transform.missing === "null") return { ...row, [transform.as]: null };
|
|
330
336
|
const timestamp = normalizeTemporalValue(row[transform.field], transform.field, index, transform.temporalUnit);
|
|
331
337
|
return {
|
|
332
338
|
...row,
|