interface ValidationResult {
valid: boolean;
errors: string[];
warnings: string[];
spec: Record<string, unknown>;
}
/**
* Validate a Vega-Lite specification
* This is a basic validation - for production, consider using the official vega-lite validator
*/
export async function validateSpec(
spec: Record<string, unknown>
): Promise<ValidationResult> {
const errors: string[] = [];
const warnings: string[] = [];
// Check required top-level properties
if (!spec.$schema) {
errors.push("Missing required property: $schema");
}
if (!spec.mark && !spec.layer && !spec.hconcat && !spec.vconcat && !spec.concat) {
errors.push("Missing required property: must have one of 'mark', 'layer', 'hconcat', 'vconcat', or 'concat'");
}
if (!spec.data) {
warnings.push("No data specified - spec may not render without data");
}
// Check mark type if present
if (spec.mark) {
const validMarks = [
"arc",
"area",
"bar",
"circle",
"geoshape",
"image",
"line",
"point",
"rect",
"rule",
"square",
"text",
"tick",
"trail",
];
const markType = typeof spec.mark === "string" ? spec.mark : (spec.mark as any)?.type;
if (markType && !validMarks.includes(markType)) {
errors.push(`Invalid mark type: '${markType}'. Valid types are: ${validMarks.join(", ")}`);
}
}
// Check encoding channels
if (spec.encoding) {
const encoding = spec.encoding as Record<string, unknown>;
const validChannels = [
"x", "y", "x2", "y2",
"color", "fill", "stroke",
"opacity", "fillOpacity", "strokeOpacity",
"size", "shape", "angle", "radius", "theta",
"text", "tooltip", "href", "url",
"row", "column", "facet",
"detail", "key", "order",
];
for (const channel of Object.keys(encoding)) {
if (!validChannels.includes(channel)) {
warnings.push(`Unknown encoding channel: '${channel}'`);
}
}
}
// Check data types in encoding
if (spec.encoding) {
const encoding = spec.encoding as Record<string, any>;
const validTypes = ["quantitative", "temporal", "nominal", "ordinal", "geojson"];
for (const [channel, def] of Object.entries(encoding)) {
if (def.type && !validTypes.includes(def.type)) {
errors.push(`Invalid type '${def.type}' in encoding channel '${channel}'. Valid types are: ${validTypes.join(", ")}`);
}
if (!def.field && !def.value && !def.datum && channel !== "detail" && channel !== "order") {
warnings.push(`Encoding channel '${channel}' has no field, value, or datum specified`);
}
}
}
// Check schema version
if (spec.$schema && typeof spec.$schema === "string") {
if (!spec.$schema.includes("vega-lite")) {
errors.push("Schema URL should point to a vega-lite schema");
}
}
return {
valid: errors.length === 0,
errors,
warnings,
spec,
};
}