Public
Edited
Mar 2
Insert cell
Insert cell
miniSchema = ({
$schema: "http://json-schema.org/draft-07/schema#",
title: "Ovarian Cancer Cohort Consortium (OC3) Data Schema",
type: "object",
additionalProperties: false,
properties: {
NEWID: {
type: "integer",
description: "Unique ID for each study participant (sequential)."
},

ENTRYAGE: {
type: ["number", "null"],
description: "Age at entry (QXAGE in years)."
},

EDUCTION: {
description: "Highest level of education",
type: ["integer", "null"],
enum: [1, 2, 3, 4, 5, 9, null],
enumDescriptions: [
"Did not finish high school (1)",
"High school (2)",
"Some college (3)",
"Completed college (4)",
"Postgraduate (5)",
"Unknown (9)",
"Missing/not provided"
]
},

HEIGHT: {
description: "Height in inches; set missing if <48 or >84.",
oneOf: [
{
type: "number",
minimum: 48,
maximum: 84
},
{ type: "null" }
]
},

BMI: {
description: "Body mass index (kg/m^2); set missing if <14 or >60.",
oneOf: [
{
type: "number",
minimum: 14,
maximum: 60
},
{ type: "null" }
]
},

ALC: {
type: ["number", "null"],
description: "Alcohol intake (grams/day). Null if missing."
},

SMOKE: {
description: "Smoking status",
type: ["integer", "null"],
enum: [0, 1, 2, null],
enumDescriptions: [
"Never (0)",
"Former (1)",
"Current (2)",
"Missing/unknown"
]
}
}
})
Insert cell
Insert cell
Insert cell
viewof table = displayTable(data, schema)
Insert cell
Insert cell
function displayTable(data, schema) {
const container = htl.html`<div id="example" style="padding-bottom:100px"></div>`;
container.innerHTML = "";

const headers = Object.keys(data[0]);

const columns = headers.map((header) => {
const propertySchema = schema.items.properties[header];
let type = "text";
if (propertySchema) {
if (
propertySchema.type === "integer" ||
propertySchema.type === "number"
) {
type = "numeric";
}
}
return { data: header, type: type };
});

const hot = new Handsontable(container, {
data: data,
columns: columns,
rowHeaders: true,
colHeaders: headers,
height: "auto",
autoWrapRow: true,
autoWrapCol: true,
contextMenu: true,
licenseKey: "non-commercial-and-evaluation"
});

const buttonWrapper = document.createElement("div");
buttonWrapper.style.textAlign = "center";
buttonWrapper.style.marginTop = "20px";

// "Validate"
const validateButton = document.createElement("button");
validateButton.innerText = "Validate";
validateButton.addEventListener("click", () => {
validateAgainstSchema(hot, schema, errorMessagesDiv);
});
buttonWrapper.appendChild(validateButton);

// "Download JSON"
const downloadButton = document.createElement("button");
downloadButton.innerText = "Download JSON";
downloadButton.style.marginLeft = "10px";
downloadButton.addEventListener("click", () => {
const editedData = hot.getSourceData();
const jsonData = JSON.stringify(editedData, null, 2);
const blob = new Blob([jsonData], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "data.json";
a.click();
URL.revokeObjectURL(url);
});
buttonWrapper.appendChild(downloadButton);

container.appendChild(buttonWrapper);

const errorContainer = document.createElement("div");
errorContainer.style.border = "1px solid #ccc";
errorContainer.style.padding = "10px";
errorContainer.style.marginTop = "20px";
errorContainer.style.height = "150px";
errorContainer.style.overflowY = "scroll";
errorContainer.style.position = "relative";

const copyButton = document.createElement("button");
copyButton.innerText = "Copy Errors";
copyButton.style.position = "absolute";
copyButton.style.top = "10px";
copyButton.style.right = "10px";
copyButton.addEventListener("click", () => {
navigator.clipboard
.writeText(errorMessagesDiv.innerText)
.then(() => {})
.catch((err) => {
console.error("Failed to copy errors: ", err);
});
});
errorContainer.appendChild(copyButton);

const errorMessagesDiv = document.createElement("div");
errorMessagesDiv.style.marginTop = "40px";
errorMessagesDiv.style.whiteSpace = "pre-wrap";
errorContainer.appendChild(errorMessagesDiv);

container.appendChild(errorContainer);

return container;
}
Insert cell
function validateAgainstSchema(hot, schema, errorMessagesDiv) {
// Clear previous error messages.
errorMessagesDiv.textContent = "";

// Remove any previous cell highlighting.
for (let row = 0; row < hot.countRows(); row++) {
for (let col = 0; col < hot.countCols(); col++) {
hot.setCellMeta(row, col, "className", "");
}
}

// Initialize Ajv and verify the schema itself.
const ajv = new Ajv({ allErrors: true });
const schemaIsValid = ajv.validateSchema(schema);
if (!schemaIsValid) {
errorMessagesDiv.textContent = "The provided schema is invalid!";
console.error("The provided schema is invalid!", ajv.errors);
hot.render();
return;
}

// Get the current table data.
const tableData = hot.getSourceData();

// Compile the schema and validate the data.
const validateFunction = ajv.compile(schema);
const valid = validateFunction(tableData);

let errorMessages = "";
if (!valid) {
validateFunction.errors.forEach((error) => {
// Example instancePath: "/0/RIDAGEYR" which refers to row 0 and property "RIDAGEYR".
const path = error.instancePath;
if (path) {
const segments = path.split("/").filter(Boolean);
if (segments.length === 2) {
const rowIndex = parseInt(segments[0], 10);
const propertyName = segments[1];
const colHeaders = hot.getColHeader();
const colIndex = colHeaders.indexOf(propertyName);
if (colIndex > -1) {
// Highlight the problematic cell.
hot.setCellMeta(rowIndex, colIndex, "className", "myInvalid");
}
}
}
errorMessages += `Error at ${error.instancePath}: ${error.message}\n`;
console.log(error);
});
} else {
errorMessages = "No errors found.";
}

// Display error messages in the error container.
errorMessagesDiv.textContent = errorMessages;

// Re-render the table to update cell highlighting.
hot.render();
}
Insert cell
Handsontable = {
const styles = html`
<link href="https://cdn.jsdelivr.net/npm/handsontable@15.0.1/dist/handsontable.full.min.css" rel="stylesheet" media="screen">
<style>
.htHighlight {
background-color: #ffeb3b66 !important;
}
.myInvalid {
background-color: #ff000033 !important;
}
</style>
`;
document.head.appendChild(styles);
return (await import("https://cdn.jsdelivr.net/npm/handsontable@15.0.1/+esm"))
.default;
}
Insert cell
Ajv = (await import("https://cdn.jsdelivr.net/npm/ajv@8.17.1/+esm")).Ajv
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more