Public
Edited
Jan 14, 2024
Importers
Insert cell
Insert cell
html`<a href="#">`.origin
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const chartJSURL = `https://cdn.jsdelivr.net/npm/chart.js/+esm`;
const chartJS = await import(chartJSURL);
return new chartJS.Chart();
}
Insert cell
Insert cell
class ChartJSVisualiser {
constructor(options) {
this.container = undefined;
this.options = options;
this.visual = undefined;
}

async build(height, width) {
try {
const ChartJS = await loadChartJSModule(); // Load Chart.js module. Subsequent loads retrieved from browser cache.
this.container = document.createElement("div");
this.container.style.background = "white";
//this.container.style.border = "1px solid #e5e7eb";
this.container.style.height = "100%"; // `${height}px`;
this.container.style.position = "relative";
this.container.style.width = `${width}px`;
const canvas = document.createElement("canvas");
this.container.appendChild(canvas);
this.visual = new ChartJS(canvas, this.options);
return this;
} catch (error) {
logError(error);
}
}
}
Insert cell
loadChartJSModule = async () => {
// Import Chart.js module.
// See: https://www.jsdelivr.com/package/npm/chart.js?path=dist for latest URL.
const chartJSURL = `https://cdn.jsdelivr.net/npm/chart.js@4.2.1/+esm`;
const chartJS = await import(chartJSURL);
const chartJSChart = chartJS.Chart;

// Register controllers, elements, scales and plugins.
chartJSChart.register(chartJS.BarController);
chartJSChart.register(chartJS.BarElement);
chartJSChart.register(chartJS.CategoryScale);
chartJSChart.register(chartJS.Legend);
chartJSChart.register(chartJS.LineController);
chartJSChart.register(chartJS.LineElement);
chartJSChart.register(chartJS.LinearScale);
chartJSChart.register(chartJS.PointElement);
chartJSChart.register(chartJS.Title);
chartJSChart.register(chartJS.Tooltip);

// Modify default options.
chartJSChart.defaults.animation = false;
chartJSChart.defaults.font.size = 16;
chartJSChart.defaults.layout.padding = 2;
chartJSChart.defaults.plugins.legend.position = "bottom";
chartJSChart.defaults.plugins.legend.labels.boxHeight = 15;
chartJSChart.defaults.plugins.legend.labels.boxWidth = 30;
chartJSChart.defaults.plugins.title.display = true;
chartJSChart.defaults.plugins.title.font.size = 20;
chartJSChart.defaults.plugins.title.font.weight = "normal";
chartJSChart.defaults.maintainAspectRatio = false;
chartJSChart.defaults.responsive = true;

// Return module.
return chartJSChart;
}
Insert cell
Insert cell
drawConnectionLines = (chart, args, options) => {
const config = chart.config;
const configData = config.data;
const configOptions = config.options;

if (!configOptions.displayConnectionLines) return;

const scales = chart.scales;
const xAxis = scales.x;
const yAxis = scales.y;

const canvasRenderingContext2D = chart.ctx;

const dataset = configData.datasets[2];
const count = dataset.data.length - 1;
for (let index = 0; index < count; index++) {
const row = dataset.data[index];
drawConnectionLine(canvasRenderingContext2D, xAxis, yAxis, row[1], index);
}
}
Insert cell
drawConnectionLine = (context, xAxis, yAxis, line, index) => {
const y1 = scaleLinear(
line,
yAxis.min,
yAxis.max,
yAxis.height,
0,
yAxis.top
);

context.save();

context.strokeStyle = "#aaa";
context.globalCompositeOperation = "destination-over";
context.lineWidth = 1;

const bandWidth = xAxis.width / xAxis.ticks.length;
const left = xAxis.left + bandWidth * index + bandWidth * 0.14;
const right = left + bandWidth * 2 - bandWidth * 0.28;

context.beginPath();
context.moveTo(left + 1, y1);
context.lineTo(right - 1, y1);
context.stroke();

context.restore();
}
Insert cell
scaleLinear = (
value,
domainStart,
domainEnd,
rangeStart,
rangeEnd,
rangeOffset
) => {
const factor = (rangeEnd - rangeStart) / (domainEnd - domainStart);
return rangeStart + factor * (value - domainStart) + rangeOffset;
}
Insert cell
Insert cell
getLegendSymbol = (legendHitBoxes, legendIndex) => {
const green =
legendIndex === 0 ? getColour("paired", 2) : getColour("paired", 3);
const orange =
legendIndex === 0 ? getColour("paired", 6) : getColour("paired", 7);

if (legendHitBoxes.length < legendIndex + 1) return undefined;
const left = legendHitBoxes[legendIndex].left;
const top = legendHitBoxes[legendIndex].top;

const canvas = document.createElement("canvas");
const context = canvas.getContext("2d");
const gradient = context.createLinearGradient(left, top, left + 30, top + 15);

gradient.addColorStop(0, green);
gradient.addColorStop(0.47, green);
gradient.addColorStop(0.47, "white");
gradient.addColorStop(0.53, "white");
gradient.addColorStop(0.53, orange);
gradient.addColorStop(1, orange);

canvas.remove();
return gradient;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
monthAbbreviations = [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
]
Insert cell
buildMeasureMap = (data, measures) => {
return data.reduce((result, row) => {
measures.forEach((measure) => {
if (measure.source) {
if (typeof measure.source === "function") {
result[measure.id].push(measure.source(row));
} else {
result[measure.id].push(row[String(measure.source)]);
}
} else {
result[measure.id].push(row[measure.id]);
}
});
return result;
}, buildMapOfArrays(measures));
}
Insert cell
buildMapOfArrays = (keys, arrayLength, itemValue) =>
keys.reduce((result, key) => {
const arr = arrayLength ? new Array(arrayLength) : [];
return {
...result,
[key.id]: arr.fill(itemValue === null ? null : itemValue)
};
}, {})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
colours = ({
opening: getColour("chartjs", 5),
starting: getColour("tableau10", 3),
hires: getColour("paired", 2),
terminations: getColour("paired", 6),
ending: getColour("tableau10", 0),
closing: getColour("tableau10", 2),
openCloseDecrease: getColour("paired", 6),
openCloseIncrease: getColour("paired", 2),
startStopDecrease: getColour("paired", 7),
startStopIncrease: getColour("paired", 3)
})
Insert cell
palettes = ({
category10: [
"#1f77b4",
"#ff7f0e",
"#2ca02c",
"#d62728",
"#9467bd",
"#8c564b",
"#e377c2",
"#7f7f7f",
"#bcbd22",
"#17becf"
],
chartjs: ["#36a2eb"],
dark2: [
"#1b9e77",
"#d95f02",
"#7570b3",
"#e7298a",
"#66a61e",
"#e6ab02",
"#a6761d",
"#666666"
],
paired: [
"#a6cee3",
"#1f78b4",
"#b2df8a",
"#33a02c",
"#fb9a99",
"#e31a1c",
"#fdbf6f",
"#ff7f00",
"#cab2d6",
"#6a3d9a",
"#ffff99",
"#b15928"
],
tableau10: [
"#4e79a7",
"#f28e2c",
"#e15759",
"#76b7b2",
"#59a14f",
"#edc949",
"#af7aa1",
"#ff9da7",
"#9c755f",
"#bab0ab"
]
})
Insert cell
getColour = (paletteId, index) => {
return palettes[paletteId][index % palettes[paletteId].length];
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
class TableVisualiser {
constructor(element, options) {
this.element = element;
this.options = options;
}

show() {
const data = this.options.data;
const columns = this.options.columns;

const wrapper = document.createElement("div");
wrapper.className = "table-wrapper";
wrapper.style.cssText = "overflow-x: scroll; padding: 10px 0 10px 10px";

const style = document.createElement("style");
style.appendChild(document.createTextNode(tableStyle));
wrapper.appendChild(style);

const tableWrapper = document.createElement("div");
tableWrapper.style.cssText = "display: flex";
const table = document.createElement("table");
table.style.cssText = "flex: 1 1 auto";
const tableRightPadding = document.createElement("div"); // Implements padding on right.
tableRightPadding.style.cssText = "flex: 0 0 10px";

const header = document.createElement("tr");
for (const column of columns) {
const th = document.createElement("th");
th.style.cssText = buildCellStyle(column);
const text = document.createTextNode(column.label);
th.append(text);
header.appendChild(th);
}
table.appendChild(header);

for (const record of data) {
const row = document.createElement("tr");
for (const column of columns) {
const td = document.createElement("td");
td.style.cssText = buildCellStyle(column);
let text;
if (typeof column.source === "function") {
text = document.createTextNode(
formatCellValue(column, column.source(record, column))
);
} else {
text = document.createTextNode(
formatCellValue(column, record[column.source])
);
}
td.appendChild(text);
row.appendChild(td);
}
table.appendChild(row);
}

tableWrapper.appendChild(table);
tableWrapper.appendChild(tableRightPadding);
wrapper.appendChild(tableWrapper);
this.element.replaceChildren(wrapper);

return this;
}

resize(items) {
return this;
}
}
Insert cell
buildCellStyle = (column) => {
switch (column.typeId) {
case "decimalNumber":
case "wholeNumber":
return ` text-align: ${column.align || "right"}`;
default:
return ` text-align: ${column.align || "left"}`;
}
}
Insert cell
formatCellValue = (column, value) => {
if (!value) return "";
switch (column.typeId) {
case "decimalNumber":
return value.toLocaleString(undefined, {
maximumFractionDigits: 2,
minimumFractionDigits: 2
});
default:
return value.toLocaleString();
}
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more