Embed
The Embed Visualisation displays your data in a visualisation of your own design, from a classic chart to an interactive artwork. A custom script determines what is displayed and how the visualisation responds to the user, all while the visualisation remains integrated in nodegoat. The script can be written by hand, or generated by any AI assistant from a single sentence that describes the visualisation.
Develop and embed a custom visualisation script. Provide a URL (https://... or a path starting with /) to a script, or the script code itself.
The script runs in its own environment and manages the visualisation through the NODEGOAT object. It receives the data as a network:
- Nodes: for the Objects and the referenced Objects that are in the data.
- Edges: for the connections made by the Object Descriptions and Sub-Object Descriptions that reference an Object.
- Statements: for the Sub-Objects with their dates and locations.
nodegoat keeps control of zoom, pan, the timeline, and the legends, and passes their state on to the script. In response to the user, the script can pass nodes and statements back to nodegoat to show the data behind them. As the script only uses the data it receives, a script can be reused for any visualisation.
Create a Visualisation
- Open the Visual Settings and go to 'Embed'. Click the context to copy it: the context contains the latest description of nodegoat's Embed Visualisation API and a reference script.
- Paste the context in any AI assistant, and describe your visualisation.
- Paste the returned script in the field 'Script', and open the Embed Visualisation to view the result.
Examples
Show every node as the first letter of its name in its colour, bouncing off the borders of the screen.
// Every active node as the first letter of its name in its colour, bouncing off the borders of the view
const elm_canvas = document.body.appendChild(document.createElement('canvas'));
const ctx = elm_canvas.getContext('2d');
const arr_letters = [];
let set_active = new Set();
NODEGOAT.on('open', function() {
for (const arr_node of NODEGOAT.DATA.nodes) { // Position and speed as a part of the view, so a resize keeps them inside
arr_letters.push({node: arr_node, letter: Array.from(arr_node.name)[0] || '?', x: Math.random(), y: Math.random(), dx: (Math.random() - 0.5) / 100, dy: (Math.random() - 0.5) / 100});
}
});
NODEGOAT.on('draw', function() {
set_active = new Set(NODEGOAT.ACTIVE.nodes);
});
NODEGOAT.on('view', function() {
elm_canvas.width = NODEGOAT.VIEW.width * NODEGOAT.VIEW.resolution;
elm_canvas.height = NODEGOAT.VIEW.height * NODEGOAT.VIEW.resolution;
elm_canvas.style.cssText = 'display: block; width: '+NODEGOAT.VIEW.width+'px; height: '+NODEGOAT.VIEW.height+'px;';
});
const render = function() {
requestAnimationFrame(render);
if (!NODEGOAT.VIEW || !NODEGOAT.ACTIVE) {
return;
}
const num_width = NODEGOAT.VIEW.width;
const num_height = NODEGOAT.VIEW.height;
ctx.setTransform(NODEGOAT.VIEW.resolution, 0, 0, NODEGOAT.VIEW.resolution, 0, 0);
ctx.clearRect(0, 0, num_width, num_height);
ctx.font = 'bold 24px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (const arr_letter of arr_letters) {
arr_letter.x += arr_letter.dx;
arr_letter.y += arr_letter.dy;
if (arr_letter.x < 0 || arr_letter.x > 1) { // A border: turn around
arr_letter.dx = -arr_letter.dx;
arr_letter.x = Math.min(Math.max(arr_letter.x, 0), 1);
}
if (arr_letter.y < 0 || arr_letter.y > 1) {
arr_letter.dy = -arr_letter.dy;
arr_letter.y = Math.min(Math.max(arr_letter.y, 0), 1);
}
if (set_active.has(arr_letter.node.id)) {
ctx.fillStyle = NODEGOAT.getNodeColor(arr_letter.node);
ctx.fillText(arr_letter.letter, 12 + arr_letter.x * (num_width - 24), 12 + arr_letter.y * (num_height - 24));
}
}
};
requestAnimationFrame(render);Show the statements as a turning time vortex of stars: the date sets the distance from the centre, the longitude sets the direction, and the active timeline lights up.
// A time vortex: every statement is a star, its distance from the centre is its date (the earliest in the middle), its direction is its longitude. Move the timeline and the active years light up
document.body.style.background = '#000000';
const elm_canvas = document.body.appendChild(document.createElement('canvas'));
const ctx = elm_canvas.getContext('2d');
let arr_stars = [];
let set_active = new Set();
const getLongitude = function(arr_geometry) { // The first coordinate pair of a GeoJSON geometry
let arr_coordinates = (arr_geometry ? arr_geometry.coordinates : null);
while (Array.isArray(arr_coordinates) && Array.isArray(arr_coordinates[0])) {
arr_coordinates = arr_coordinates[0];
}
return (Array.isArray(arr_coordinates) ? arr_coordinates[0] : null);
};
NODEGOAT.on('open', function() {
arr_stars = NODEGOAT.DATA.statements.filter(arr_statement => arr_statement.date[0]).map(function(arr_statement, i) {
const num_longitude = getLongitude(arr_statement.geometry);
return {statement: arr_statement, time: NODEGOAT.getDate(arr_statement.date[0]).getTime(), angle: (num_longitude !== null ? num_longitude / 180 * Math.PI : i * 2.39996), color: NODEGOAT.getStatementColor(arr_statement)}; // Placed in time through getDate. Without a location: spread by the golden angle
});
});
NODEGOAT.on('draw', function() {
set_active = new Set(NODEGOAT.ACTIVE.statements);
});
NODEGOAT.on('view', function() {
elm_canvas.width = NODEGOAT.VIEW.width * NODEGOAT.VIEW.resolution;
elm_canvas.height = NODEGOAT.VIEW.height * NODEGOAT.VIEW.resolution;
elm_canvas.style.cssText = 'display: block; width: '+NODEGOAT.VIEW.width+'px; height: '+NODEGOAT.VIEW.height+'px;';
});
const render = function(num_time) {
requestAnimationFrame(render);
if (!NODEGOAT.VIEW || !NODEGOAT.ACTIVE) {
return;
}
const num_width = NODEGOAT.VIEW.width;
const num_height = NODEGOAT.VIEW.height;
const num_x = num_width / 2 + NODEGOAT.VIEW.offset_x; // Zoom and pan as nodegoat sets them
const num_y = num_height / 2 + NODEGOAT.VIEW.offset_y;
const num_radius = Math.min(num_width, num_height) / 2 * 0.9 * NODEGOAT.VIEW.scale;
const num_min = NODEGOAT.getDate(NODEGOAT.ACTIVE.bounds.min).getTime();
const num_span = (NODEGOAT.getDate(NODEGOAT.ACTIVE.bounds.max).getTime() - num_min) || 1;
const num_spin = num_time / 20000; // The vortex turns slowly
ctx.setTransform(NODEGOAT.VIEW.resolution, 0, 0, NODEGOAT.VIEW.resolution, 0, 0);
ctx.globalCompositeOperation = 'source-over';
ctx.fillStyle = 'rgba(0, 0, 0, 0.2)'; // Fade the previous frame, leaving trails
ctx.fillRect(0, 0, num_width, num_height);
ctx.globalCompositeOperation = 'lighter'; // Overlapping stars add up to light
for (const arr_star of arr_stars) {
const num_r = (arr_star.time - num_min) / num_span * num_radius;
const num_angle = arr_star.angle + num_spin;
const is_active = set_active.has(arr_star.statement.id);
const num_size = (is_active ? 3 + 1.5 * Math.sin(num_time / 200 + arr_star.angle * 7) : 1); // Active stars twinkle
ctx.fillStyle = (is_active ? arr_star.color : 'rgba(255, 255, 255, 0.15)');
ctx.fillRect(num_x + Math.cos(num_angle) * num_r - num_size / 2, num_y + Math.sin(num_angle) * num_r - num_size / 2, num_size, num_size);
}
ctx.save();
ctx.lineWidth = 2;
ctx.strokeStyle = 'rgba(200, 230, 255, 0.1)';
ctx.shadowColor = 'rgba(80, 180, 255, 1)'; // A soft blue glow, building up with the trails
ctx.shadowBlur = 16;
for (const num_date of [NODEGOAT.ACTIVE.range.min, NODEGOAT.ACTIVE.range.max]) { // The range of the timeline as two rings
ctx.beginPath();
ctx.arc(num_x, num_y, Math.max((NODEGOAT.getDate(num_date).getTime() - num_min) / num_span, 0) * num_radius, 0, Math.PI * 2);
ctx.stroke();
}
ctx.restore(); // The glow stays with the rings, the stars of the next frame are drawn without it
};
requestAnimationFrame(render);