Public
Edited
Mar 7, 2023
Insert cell
md`# Node editor





`

Insert cell
md`## Visual elements




`
Insert cell
md`### Nodes`
Insert cell
drawNode = (context, nodeIdx, node, state, style) => {
const pinSep = 15;
const pin = {w: 15, h: 15};
const mouse = state.mouse;
// draw node
context.fillStyle = style.nodeBackground;
context.fillRect(node.x, node.y, node.width, node.height);

if (!node.parameter_in) {
node.parameter_in = []
}
var idx = 0;
for (var p_in of node.parameter_in) {
const x = node.x - pinSep;
const y = node.y + idx * pinSep;
const subdivision = Math.max(10, node.parameter_in.length)
const colors = d3.quantize(d3.interpolateHcl("#60c96e", "#4d4193"), subdivision);

context.fillStyle = colors[idx];
context.fillRect(x, y, pin.w, pin.h);
idx++;
}
var idx = 0;
for (var input of node.inputs) {
const subdivision = Math.max(10, node.inputs.length)
const colors = d3.quantize(d3.interpolateHcl("#60c96e", "#4d4193"), subdivision);
const x = node.x + idx * pinSep;
const y = node.y;

if (mouse.x > x && mouse.x < x + 15 && mouse.y > y && mouse.y < y + 15) {
context.fillStyle = colors[idx];
context.fillRect(x-4, node.y-4, pin.w+4, pin.h+4);
if (input.name) {
context.font = '18px sans-serif';
context.fillStyle = style.nodeLabelColor;
context.fillText(input.name, x, y-4)
}
} else {
context.fillStyle = colors[idx];
context.fillRect(x, node.y, pin.w, pin.h);
}
idx++;
}

var idx = 0;
for (var output of node.outputs) {
const x = node.x + idx * pinSep;
const y = node.y + node.height-pin.h;
const subdivision = Math.max(10, node.outputs.length)
const colors = d3.quantize(d3.interpolateHcl("#f4e153", "#362142"), subdivision);
if (mouse.x > x && mouse.x < x + 15 && mouse.y > y && mouse.y < y + 15) {
// draw the node slightly larger
context.fillStyle = colors[idx];
context.fillRect(x-4, y, pin.w+4, pin.h+4);
if (output.name) {
context.font = '18px sans-serif';
context.fillStyle = style.nodeLabelColor;
context.fillText(output.name, x, y + 15 +18)
}
} else {
if (state.connecting.active && state.connecting.start.node == nodeIdx && state.connecting.start.output == idx) {
context.fillStyle = "blue";
} else {
context.fillStyle = colors[idx];
}
context.fillRect(x, y, pin.w, pin.h);
}
idx++;
}
if (node.active) {
context.strokeStyle = "#aaa";
context.strokeRect(node.x - 3, node.y - 3, node.width + 6, node.height + 6);
}
context.font = '18px sans-serif';
context.fillStyle = style.nodeLabelColor;
context.fillText(node.name, node.x + node.width + 10, node.y + node.height/2 + 4)
}
Insert cell
maxDistance = Infinity
Insert cell
md`### Blocks`
Insert cell
drawBlock = (context, block, state, style) => {
const pinSep = 15;
const mouse = state.mouse;
const pin = {w: 15, h:15};

context.strokeStyle = "#aaa";
context.strokeRect(block.x, block.y, 200, 200);

if (mouse.x > block.x
&& mouse.x < block.x + block.width
&& mouse.y > block.y
&& mouse.y < block.y + 15) {
context.strokeStyle = "white";
context.fillRect(block.x, block.y, 200, 15);
}

if (mouse.x > block.x
&& mouse.x < block.x + block.width
&& mouse.y > block.y + block.height - 15
&& mouse.y < block.y + block.height) {
context.strokeStyle = "white";
context.fillRect(block.x, block.y + block.height - 15, 200, 15);
}

var idx = 0;
for (var input of block.inputs) {
const subdivision = Math.max(10, block.inputs.length)
const colors = d3.quantize(d3.interpolateHcl("#60c96e", "#4d4193"), subdivision);
const x = block.x + idx * pinSep;
const y = block.y;

if (mouse.x > x && mouse.x < x + 15 && mouse.y > y && mouse.y < y + 15) {
context.fillStyle = colors[idx];
context.fillRect(x-4, block.y-4, pin.w+4, pin.h+4);
if (input.name) {
context.font = '18px sans-serif';
context.fillStyle = style.nodeLabelColor;
context.fillText(input.name, x, y-4)
}
} else {
context.fillStyle = colors[idx];
context.fillRect(x, block.y, pin.w, pin.h);
}
idx++;
}
var idx = 0;
for (var output of block.outputs) {
const x = block.x + idx * pinSep;
const y = block.y + block.height-pin.h;
const subdivision = Math.max(10, block.outputs.length)
const colors = d3.quantize(d3.interpolateHcl("#f4e153", "#362142"), subdivision);
if (mouse.x > x && mouse.x < x + 15 && mouse.y > y && mouse.y < y + 15) {
// draw the pin slightly larger
context.fillStyle = colors[idx];
context.fillRect(x-4, y, pin.w+4, pin.h+4);
} else {
context.fillStyle = colors[idx];
context.fillRect(x, y, pin.w, pin.h);
}
idx++;
}

}
Insert cell
md`### Connections`
Insert cell
drawConnection = (context, connection, state) => {
const startNode = state.nodes[connection.start];
const endNode = state.nodes[connection.end];
const mouse = state.mouse;
const pinSep = 15;
const pinWidth = 15;
const pinHeight = 15;
const nodeOffset = 20;
const x = endNode.x + connection.input * pinSep;
const y = endNode.y;
context.beginPath();
context.strokeStyle = "#aaa";
if (mouse.x > x && mouse.x < x + 15 && mouse.y > y && mouse.y < y + 15) {
// highlight connections that would be disconnected
if (state.connecting.active) {
context.strokeStyle = "red";
} else {
context.strokeStyle = "yellow";
}
}

if (startNode.y >= endNode.y) {
const minX = Math.min(endNode.x, startNode.x);
context.moveTo(startNode.x + connection.output * pinSep + pinWidth/2, startNode.y + startNode.height);
context.lineTo(startNode.x + connection.output * pinSep + pinWidth/2, startNode.y + startNode.height + nodeOffset + connection.output * pinSep);
context.lineTo(minX - nodeOffset, startNode.y + startNode.height + nodeOffset + connection.output * pinSep);
context.lineTo(minX - nodeOffset, endNode.y - nodeOffset - connection.input * pinSep);
context.lineTo(endNode.x + connection.input * pinSep + pinWidth/2, endNode.y - nodeOffset - connection.input * pinSep);
context.lineTo(endNode.x + connection.input * pinSep + pinWidth/2, endNode.y);
} else {
context.moveTo(startNode.x + connection.output * pinSep + pinWidth/2, startNode.y + startNode.height);
context.lineTo(endNode.x + connection.input * pinSep + pinWidth/2, endNode.y);
}
context.lineWidth = 2;
context.stroke();
}
Insert cell
node.canvas
Insert cell
state = node.state
Insert cell
d3 = require("d3-selection@2", "d3-scale-chromatic@2", "d3-drag@2", "d3-array@2", "d3-zoom@2", "d3-color@2", "d3-interpolate@2")
Insert cell
node = {
const canvas = document.createElement('canvas');
canvas.tabindex = 1;
const context = canvas.getContext('2d');
const width = 1600;
const height = 1200;
canvas.width = width;
canvas.height = height;
// initial node state
const graphStyle = {
backgroundColor: "#eaeaea",// "#151515",
nodeBackground: "#6a6a6a",
nodeInputColors: d3.quantize(d3.interpolateHcl("#f4e153", "#362142"), 10),
nodeOutputColors: d3.quantize(d3.interpolateHcl("#f4e153", "#362142"), 10),
nodeLabelColor: "#000000",
sidebarColor: "#3f3f3f",
sidebarWidth: 30,
};
const nodes = [{
x: 40,
y: 0,
width: 100,
height: 30,
inputs: [
{name: "in0"},
{name: "in1"}
],
outputs: [
{name: "out0"},
{name: "out1"},
{name: "out2"}
],
parameter_in: [
// {name: "p_in0"},
],
parameter_out: [
// {name: "p_out0"}
],
active: false,
name: "conv2d"
},
{
x: 140,
y: 40,
width: 100,
height: 30,
inputs: [0],
outputs: [
{name: "out1"},
{name: "out2"}
],
subNodes: [],
active: false,
name: "relu"
},
{
x: 200,
y: 100,
width: 100,
height: 30,
inputs: [0, 2],
outputs: [3, 4, 5, 8],
subNodes: [],
active: false,
name: "conv2d"
},
{
x: 200,
y: 200,
width: 100,
height: 30,
inputs: [0, 2],
outputs: [3, 4, 5, 8],
active: false,
name: "relu"
},
{
x: 400,
y: 400,
width: 100,
height: 30,
inputs: [0, 1, 2, 3],
outputs: [3, 4, 5, 8, 9],
active: false,
name: "relu"
}
];
const connections = [];
// editor state
const state = {
nodes: nodes,
blocks: [],
connections: connections,
blockNodeConnections: [],
connecting: {active: false, start: {x:0, y:0, node:0, output:-1, input: -1}, x:0, y:0},
selectedNodes: [],
selection: {active: false, x:0, y:0, start: {x:0, y:0}},
mouse: {x: 0, y: 0},
transform: d3.zoomIdentity,
keypressed: {}
}
for (var node of nodes) {
const length = Math.max(node.inputs.length, node.outputs.length);
node.width = length > 1 ? (length-1) * (15 + 2) + 15 : 15;
}
function render() {
const {x, y, k} = state.transform;

// background
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = graphStyle.backgroundColor;
context.fillRect(0, 0, canvas.width, canvas.height);
// nodes + connections
context.save();
context.translate(x, y);
context.scale(k, k);

// nodes
nodes.forEach((node,idx) => drawNode(context, idx, node, state, graphStyle));
// blocks
state.blocks.forEach((block, idx) => drawBlock(context, block, state, graphStyle));
// connections
connections.forEach((conn, idx) => drawConnection(context, conn, state));
for (var nodeIdx of state.selectedNodes) {
const node = nodes[nodeIdx];
context.strokeStyle = "#aaa";
context.strokeRect(node.x - 3, node.y - 3, node.width + 6, node.height + 6);
}
if (state.connecting.active) {
context.beginPath();
context.moveTo(state.connecting.start.x, state.connecting.start.y);
context.lineTo(state.connecting.x, state.connecting.y);
context.strokeStyle = "#aaa";
context.stroke();
}
if (state.selection.active) {
context.strokeStyle = '#aaa';
context.beginPath();
context.moveTo(state.selection.start.x, state.selection.start.y);
context.lineTo(state.selection.x, state.selection.start.y);
context.lineTo(state.selection.x, state.selection.y);
context.lineTo(state.selection.start.x, state.selection.y);
context.lineTo(state.selection.start.x, state.selection.start.y);
context.stroke();
}
context.restore();
}
// drag + zoom handling
d3.select(context.canvas)
.call(drag(state).on("start.render drag.render end.render", render))
.call(d3.zoom()
.scaleExtent([1/8, 8])
.on("zoom", ({transform}) => {
state.transform = transform;
render();
}));
// keep track of mouse state
d3.select(context.canvas).on("mousemove", (event) => {
var pos = d3.pointer(event);
state.mouse.x = state.transform.invertX(pos[0]);
state.mouse.y = state.transform.invertY(pos[1]);
render();
});

render();
return {canvas: canvas, state: state};
}
Insert cell
drag = state => {
function dragsubject(event) {
let subject = null;
let subjectIsOutput = false;
let subjectIsInput = false;
let distance = maxDistance;
var nodeIdx = 0;
const ex = state.transform.invertX(event.x);
const ey = state.transform.invertY(event.y);
const pinSep = 15;
const pin = {w: 15, h: 15};
for (const c of state.nodes) {
var idx = 0;

// detect intersection of cursor with outputs
for (var i in c.outputs) {
const cx = c.x + idx*pinSep + pin.w/2;
const cy = c.y + c.height - pin.h/2;
if ((Math.abs(ex - cx) < pin.w/2) && (Math.abs(ey - cy) < pin.h/2) && (state.selectedNodes.length === 0)) {
subject = state.connecting;
subject.start.x = cx;
subject.start.y = cy;
subject.start.node = nodeIdx;
subject.start.output = idx;
subject.start.input = -1;
subject.x = state.transform.applyX(cx);
subject.y = state.transform.applyY(cy);
subjectIsOutput = true;
break;
}
idx++;
}
idx = 0;
// detect intersection of cursor with inputs
for (var i in c.inputs) {
const cx = c.x + idx*pinSep + pin.w/2;
const cy = c.y + pin.h/2;
if ((Math.abs(ex - cx) < pin.w/2) && (Math.abs(ey - cy) < pin.h/2) && (state.selectedNodes.length === 0)) {
subject = state.connecting;
subject.start.x = cx;
subject.start.y = cy;
subject.start.node = nodeIdx;
subject.start.input = idx;
subject.start.output = -1;
subject.x = state.transform.applyX(cx);
subject.y = state.transform.applyY(cy);
subjectIsInput = true;
break;
}
idx++;
}
if ((subjectIsOutput || subjectIsInput) && subject) {
break;
}

if ((c.x < ex) && (ex < c.x+c.width) && (c.y < ey) && (ey < c.y+c.height)) {
subject = c;
subject.x = state.transform.applyX(c.x);
subject.y = state.transform.applyY(c.y);
break;
}
nodeIdx++;
}

if (!subject && event.sourceEvent.shiftKey) {
subject = state.selection;
subject.start.x = ex;
subject.start.y = ey;
subject.x = state.transform.applyX(ex);
subject.y = state.transform.applyY(ey);
subject.active = true;
}
return subject;
}

function dragstarted({subject}) {
subject.x = state.transform.invertX(subject.x);
subject.y = state.transform.invertY(subject.y);
subject.active = true;
}

// When dragging, update the subject’s position.
function dragged(event) {
const ex = event.x;
const ey = event.y;
const edx = event.dx;
const edy = event.dy;
const {x, y, k} = state.transform;

// translate all selected nodes
if (!state.selection.active) {
for (var nodeIdx of state.selectedNodes) {
state.nodes[nodeIdx].x += 1/k * edx;
state.nodes[nodeIdx].y += 1/k * edy;
}
}
event.subject.x = state.transform.invertX(ex);
event.subject.y = state.transform.invertY(ey);
state.mouse.x = state.transform.invertX(ex);
state.mouse.y = state.transform.invertY(ey);
}

// When ending a drag gesture, mark the subject as inactive again.
function dragended({subject}) {
const pinSep = 15;
const pin = {w: 15, h: 15};
// handle selection
if (state.selection.active) {
state.selectedNodes = [];
const s = state.selection;
var nodeIdx = 0;
for (var i = 0; i < state.nodes.length; i++) {
const n = state.nodes[i];
// handle reversal in rectangle
const sx = s.start.x > s.x ? s.x : s.start.x;
const sy = s.start.y > s.y ? s.y : s.start.y;
const sxm = s.start.x > s.x ? s.start.x : s.x;
const sym = s.start.y > s.y ? s.start.y : s.y;
// compute overlap
const x_overlap = Math.max(0, Math.min(sxm, n.x+n.width) - Math.max(sx, n.x))
const y_overlap = Math.max(0, Math.min(sym, n.y+n.height) - Math.max(sy, n.y));
if (x_overlap * y_overlap > 0.1) {
state.selectedNodes.push(i);
}
}
}

// handle connection
if (state.connecting.active) {
var nodeIdx = 0;
for (const c of state.nodes) {
if (subject.start.output >= 0) {
var idx = 0;
for (var i in c.inputs) {
const cx = c.x + idx*pinSep + pin.w/2;
const cy = c.y + pin.h/2;
if ((Math.abs(subject.x - cx) < pin.w/2) && (Math.abs(subject.y - cy) < pin.h/2)) {
state.connections.push(
{
start: subject.start.node,
end: nodeIdx,
output: subject.start.output,
input: idx
}
);
break;
}
idx++;
}
} else if (subject.start.input >= 0) {
var idx = 0;
for (var i in c.outputs) {
const cx = c.x + idx*pinSep + pin.w/2;
const cy = c.y + c.height - pin.h/2;
if ((Math.abs(subject.x - cx) < pin.w/2) && (Math.abs(subject.y - cy) < pin.h/2)) {
state.connections.push(
{
end: subject.start.node,
start: nodeIdx,
output: idx,
input: subject.start.input
}
);
break;
}
idx++;
}
}
nodeIdx++;
}
var blockIdx;
for (const b of state.blocks) {
if (subject.start.output >= 0) {
var idx = 0;
for (var i in b.outputs) {
const cx = b.x + idx*pinSep + pin.w/2;
const cy = b.y + b.height - pin.h/2;
if ((Math.abs(subject.x - cx) < pin.w/2) && (Math.abs(subject.y - cy) < pin.h/2)) {
break;
}
idx++;
}
} else if (subject.start.input >= 0) {
}
blockIdx++;
}
}
subject.active = false;
}

return d3.drag()
.subject(dragsubject)
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
}
Insert cell
roundedRect = (ctx, x, y, w, h, r) => {
if (w < 2 * r) r = w / 2;
if (h < 2 * r) r = h / 2;
ctx.beginPath();
ctx.moveTo(x+r, y);
ctx.arcTo(x+w, y, x+w, y+h, r);
ctx.arcTo(x+w, y+h, x, y+h, r);
ctx.arcTo(x, y+h, x, y, r);
ctx.arcTo(x, y, x+w, y, r);
ctx.closePath();
}
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