Public
Edited
Jul 22, 2023
1 fork
Importers
5 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
config = ({
rows: ["2.5rem", "2rem", "1fr", "30px"],
columns: ["150px", "1fr", "5rem"],
areas: [
["head", "head", "head"],
["topMenu", "topMenu", "topMenu"],
["leftPanel", "body", "rightPanel"],
["leftPanel", "footer", "footer"]
],
children: {
head: {
rows: ["1fr"],
columns: ["150px", "1fr", "8rem"],
areas: [["logo", ".", "searchBar"]]
},
body: {
rows: ["2em", "1fr"],
columns: ["1fr", "1.5fr"],
areas: [
["title", "title"],
["leftColumn", "rightColumn"]
]
}
}
})
Insert cell
boxLayout(boxWidth, boxHeight)
Insert cell
Insert cell
Insert cell
Insert cell
minBoxSizes = {
return {
width: maxBoxSizes.width * 0.5,
height: maxBoxSizes.height * 0.25
};
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
buildHtmlLayout({
config,
newArea(key, style) {
const div = document.createElement("div");
Object.assign(div.style, style);
return div;
},
newContainer(key, style, children = []) {
const div = document.createElement("div");
Object.assign(div.style, style);
for (const child of children) {
div.appendChild(child);
}
return div;
},
newPlaceholder(key) {
const div = document.createElement("div");
return div;
}
})
Insert cell
function buildHtmlLayout({
config,
newArea,
newPlaceholder,
newContainer
} = {}) {
let counter = 0;
config = getLayoutConfig(config);
return buildContainer(null, config);

function buildContainer(containerKey, config) {
const children = [];
const areaIndex = {};
const containerIndex = {};
for (const row of config.areas) {
for (const areaKey of row) {
if (areaKey in areaIndex || areaKey in containerIndex) continue;
let elm;
if (areaKey === ".") {
elm = newPlaceholder(`area-${counter++}`);
} else {
if (config.children && config.children[areaKey]) {
const { root, areas, containers } = buildContainer(
areaKey,
config.children[areaKey]
);
elm = containerIndex[areaKey] = root;
Object.assign(areaIndex, areas);
Object.assign(containerIndex, containers);
} else {
elm = areaIndex[areaKey] = newArea(areaKey, {
gridArea: areaKey
});
}
}
children.push(elm);
}
}
const formatSizes = (list) =>
list.map(({ unit, value }) => `${value}${unit}`).join(" ");
const formatAreas = (list) =>
list.map((areas) => `"${areas.join(" ")}"`).join("\n");
const containerStyle = containerKey ? { gridArea: containerKey } : {};
Object.assign(containerStyle, {
display: "grid",
gridTemplateAreas: formatAreas(config.areas),
gridTemplateRows: formatSizes(config.rows),
gridTemplateColumns: formatSizes(config.columns)
});
return {
root: newContainer(containerKey, containerStyle, children),
areas: areaIndex,
containers: containerIndex
};
}
}
Insert cell
Insert cell
{
const applyLayout = buildLayout(config);
return applyLayout(1024, 768);
}
Insert cell
function buildLayout(config, sizes) {
config = getLayoutConfig(config);
const index = getAreasIndex(config);
return (width, height) => {
let config = { index, sizes };
if (typeof width === "object") {
Object.assign(config, width);
} else {
Object.assign(config, { width, height });
}
return newLayout(config);
};
}
Insert cell
Insert cell
{
const containerWidth = 1024;
const containerHeight = 768;
return newLayout({
index: getAreasIndex(getLayoutConfig(config)),
width: containerWidth,
height: containerHeight
});
}
Insert cell
function newLayout({ index, top = 0, left = 0, width, height, sizes = {} }) {
sizes = Object.assign(
{
px: 1,
rem: 16,
em: 16
},
sizes
);
const horizontal = getUnitsSizes(index.width, width, sizes);
const vertical = getUnitsSizes(index.height, height, sizes);

const result = {
areas: {}
};
for (let [areaKey, area] of Object.entries(index.areas)) {
const absAreaInfo = {
top: top + calculateSize(vertical, area.top),
left: left + calculateSize(horizontal, area.left),
height: calculateSize(vertical, area.height),
width: calculateSize(horizontal, area.width)
};
result.areas[areaKey] = absAreaInfo;
}
let children;
if (index.children) {
children = {};
for (let [areaKey, areaConfig] of Object.entries(index.children)) {
const area = result.areas[areaKey];
if (!area) continue; // TODO: add a warning or something like that?
children[areaKey] = newLayout({ index: areaConfig, ...area, sizes });
}
}
if (children) result.children = children;

return result;

function getUnitsSizes(units, fullSize, unitSizes) {
const fixed =
(units.px || 0) * unitSizes.px +
(units.rem || 0) * unitSizes.rem +
(units.em || 0) * unitSizes.em;
return {
...unitSizes,
fr: units.fr > 0 ? Math.max(0, fullSize - fixed) / units.fr : 0
};
}
function calculateSize(units, value) {
return (
units.px * (value.px || 0) +
units.rem * (value.rem || 0) +
units.em * (value.em || 0) +
units.fr * (value.fr || 0)
);
}
}
Insert cell
Insert cell
getLayoutConfig(config)
Insert cell
/**
* Returns a normalized layout configuration where rows, columns and areas are represented as a Javascript arrays.
*/
function getLayoutConfig(config) {
const result = {};
if (!config.rows) throw new Error("Rows are not defined");
result.rows = splitSizes(config, "rows");
if (!config.columns) throw new Error("Columns are not defined");
result.columns = splitSizes(config, "columns");
if (!config.areas) throw new Error("Areas are not defined");
result.areas = splitLines(config.areas);
if (result.areas.length !== result.rows.length) {
throw new Error(
`Area definition has a wrong number of rows. \nExpected ${
result.rows.length
} rows but found ${result.areas.length}:\n- ${result.areas
.map((row) => JSON.stringify(row))
.join("\n- ")}.`
);
}
for (let i = 0; i < result.rows.length; i++) {
const row = result.areas[i];
if (row.length !== result.columns.length)
throw new Error(
`Area definition in the row ${i} has a wrong number of columns: \nExpected ${
result.columns.length
} columns but found ${row.length}.\nRow: ${JSON.stringify(row)}.`
);
}
if (config.children) {
let children;
for (const [key, child] of Object.entries(config.children)) {
children = children || {};
children[key] = getLayoutConfig(child);
}
if (children) result.children = children;
}
return result;

function splitSizes(config, name) {
const array = splitCells(config[name]);
const sizes = new Array(array.length);
for (let i = 0; i < array.length; i++) {
let size = (sizes[i] = {});
const val = array[i];
if (typeof val === "object") {
size.value = val.value;
size.unit = val.unit;
} else {
val.replace(/^(\d+(\.\d+)?)(\w+)$/g, (match, v, dec, unit) => {
v = +v;
size.unit = unit;
size.value = v;
});
}
if (isNaN(size.value)) {
throw new Error(
`"${name}": value should be a number. Position ${i}. Value: "${size.value}".`
);
}
if (typeof size.unit !== "string") {
throw new Error(
`"${name}": unit should be defined as a string (ex: "px", "fr"). Position ${i}. Unit: "${size.unit}".`
);
}
}
return sizes;
}
function splitLines(value) {
value = Array.isArray(value)
? value
: value.split(",").map((_) => _.trim());
return value.map(splitCells);
}
function splitCells(value) {
return Array.isArray(value)
? value
: value.split(/\s+/).map((_) => _.trim());
}
}
Insert cell
Insert cell
getAreasIndex(getLayoutConfig(config))
Insert cell
/**
* This method transforms a valid are configuration to an index of non-intersecting areas
*/
function getAreasIndex(config) {
const index = {};
config.areas.forEach((row, rowId) => {
row.forEach((key, colId) => {
if (key === ".") return;
let area;
if ((area = index[key])) {
area.rows = [
Math.min(rowId, area.rows[0]),
Math.max(rowId, area.rows[1])
];
area.columns = [
Math.min(colId, area.columns[0]),
Math.max(colId, area.columns[1])
];
} else {
index[key] = {
rows: [rowId, rowId],
columns: [colId, colId]
};
}
});
});

// Postprocessing areas:
// - validate that there is no intersections
// - update positions and dimentions of each area
// - calculate child layouts
let children;
for (let [key, area] of Object.entries(index)) {
// Check that there is no intersections between areas defninitions
for (let rowId = area.rows[0]; rowId <= area.rows[1]; rowId++) {
for (let colId = area.columns[0]; colId <= area.columns[1]; colId++) {
const areaKey = config.areas[rowId][colId];
if (areaKey !== key) {
const errorMsg =
`Areas "${areaKey}" and "${key}" ` +
`intersect at [${rowId}, ${colId}].`;
throw new Error(errorMsg);
}
}
}
// Calculate position and dimentions of this area
updatePositionAndDimentions(config, area);

// Calculate child layouts
if (config.children && config.children[key]) {
children = children || {};
children[key] = getAreasIndex(config.children[key]);
}
}
const result = updatePositionAndDimentions(config, {
areas: index,
rows: [0, config.rows.length - 1],
columns: [0, config.columns.length - 1]
});
if (children) result.children = children;
return result;

function updatePositionAndDimentions(config, area) {
area.top = {};
area.left = {};
area.width = {};
area.height = {};
for (let y = 0; y < area.rows[0]; y++) update(area.top, config.rows[y]);
for (let y = area.rows[0]; y <= area.rows[1]; y++)
update(area.height, config.rows[y]);
for (let x = 0; x < area.columns[0]; x++)
update(area.left, config.columns[x]);
for (let x = area.columns[0]; x <= area.columns[1]; x++)
update(area.width, config.columns[x]);
return area;

function update(dim, { unit, value } = {}) {
if (!unit) return;
dim[unit] = (dim[unit] || 0) + value;
}
}
}
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