Public
Edited
Apr 5
Importers
Insert cell
Insert cell
Insert cell
Plot = () => {
const cmds = [];
const stack = [];
let pos = [0, 0];
const movePos = (dx, dy) => (pos = add(pos, [dx, dy]));
const move = (dx, dy) => {
movePos(dx, dy);
const last = cmds.at(-1);
if (last?.mv) last.mv = add(last.mv, [dx, dy]);
else cmds.push({ mv: [dx, dy] });
};
const draw = (dx, dy, angle) => (
movePos(dx, dy), cmds.push({ dr: angle ? [dx, dy, angle] : [dx, dy] })
);
const vec = (dx, dy, angle) => {
const [cos, sin] = [Math.cos(angle), Math.sin(angle)];
return [dx * cos - dy * sin, dx * sin + dy * cos];
};
const vect = ([x1, y1], [x2, y2]) => [x2 - x1, y2 - y1];
const push = () => stack.push(pos);
const pop = () => cmds.push({ mv: vect(pos, (pos = stack.pop())) });
const add = ([x, y], [dx, dy]) => [x + dx, y + dy];
const mul = ([x, y], r) => (r == 1 ? [x, y] : [x * r, y * r]);
const chain = (plot) => {
for (const cmd of plot) {
if (cmd.mv) move(...cmd.mv);
else draw(...cmd.dr);
}
};
const plot = {
move: (dx, dy) => ((dx != 0 || dy != 0) && move(dx, dy), plot),
draw: (dx, dy, angle) => (
(dx != 0 || dy != 0) && draw(dx, dy, angle), plot
),
text: (lines, options) => {
const defaults = {
halign: "center",
valign: "middle",
inter: 0,
scale: [1, 1],
rotation: 0
};
const { halign, valign, inter, scale, rotation } = Object.assign(
defaults,
options
);
const LINE_HEIGHT = 32;
const ht = LINE_HEIGHT * scale[1];
const it = inter * scale[1];
const nb = lines.length;
const h = ht * nb + it * (nb - 1);
const dys = { top: 0, middle: -h / 2, bottom: -h };
const dy = dys[valign];
const angle = rotation * (Math.PI / 180);
push();
move(...vec(0, dy + ht / 2, angle));
for (const str of lines) {
const { x: xt, width: wt } = bounds(str, options);
const dxs = { left: 0, center: -wt / 2, right: -wt };
const dx = dxs[halign];
push();
move(...vec(dx - xt, 0, angle));
let [cx, cy] = [0, 0];
for (const path of str2paths(str, options)) {
if (path.length > 0) {
const [px, py] = path[0];
move(...vec(-cx + (cx = px), -cy + (cy = py), angle));
for (const [px, py] of path.slice(1)) {
draw(...vec(-cx + (cx = px), -cy + (cy = py), angle));
}
}
}
pop();
move(...vec(0, ht, angle));
}
pop();
return plot;
},
push: () => (push(), plot),
pop: () => (pop(), plot),
chain: (p) => (chain(p), plot),
insert: (p) => plot.push().chain(p).pop(),
[Symbol.iterator]: () => cmds[Symbol.iterator]()
};
return plot;
}
Insert cell
translate = (plot, vector) =>
Plot()
.move(...vector)
.chain(plot)
Insert cell
polyline = (pts, closed) => {
if (closed) pts.push(pts[0]);
const p = Plot().move(...pts[0]);
const vect = ([x1, y1], [x2, y2]) => [x2 - x1, y2 - y1];
for (let i = 0; i < pts.length - 1; i++) p.draw(...vect(pts[i], pts[i + 1]));
return p;
}
Insert cell
transform = (plot, fn) => {
const tp = Plot();
for (const cmd of plot) {
if (cmd.mv) tp.move(...fn(cmd.mv));
else {
const [dx, dy, angle] = cmd.dr;
tp.draw(...fn([dx, dy]), angle);
}
}
return tp;
}
Insert cell
scale = (plot, ratio) =>
ratio == 1
? Plot().chain(plot)
: transform(plot, ([x, y]) => [ratio * x, ratio * y])
Insert cell
dash = (plot, param) => {
let dist = 0;
const [down, up] = param || [4, 6];
const dp = Plot();
const draw = (dx, dy, angle) => {
let d = Math.hypot(dx, dy);
const [i, j] = [dx / d, dy / d];
let m = dist % (down + up);
while (d > 0) {
if (m < down) {
const dd = Math.min(down - m, d);
dp.draw(dd * i, dd * j);
dist += dd;
d -= dd;
m = down;
}
if (d > 0) {
const du = Math.min(down + up - m, d);
dp.move(du * i, du * j);
dist += du;
d -= du;
m = 0;
}
}
};
for (const cmd of plot) {
if (cmd.dr) {
draw(...cmd.dr);
} else {
dist = 0;
dp.move(...cmd.mv);
}
}

return dp;
}
Insert cell
crop = (plot, [xmin, xmax], [ymin, ymax], epsilon = 1e-6) => {
const add = ([x, y], [dx, dy]) => [x + dx, y + dy];
const vec = ([x1, y1], [x2, y2]) => [x2 - x1, y2 - y1];
const intersect = intersectRect(
[(xmin + xmax) / 2, (ymin + ymax) / 2],
[xmax - xmin, ymax - ymin],
1e-10 // Number.EPSILON is too small
);
let [x1, y1] = [0, 0];
let x2, y2;
const cp = Plot();
for (const cmd of plot) {
const [dx, dy] = cmd.mv || cmd.dr;
[x2, y2] = add([x1, y1], [dx, dy]);
//console.log("pt1,pt2", [x1, y1], [x2, y2], cmd.dr);
if (cmd.dr) {
const [pt1, pt2] = [
[x1, y1],
[x2, y2]
];
const [pta, ptb] = intersect([pt1, pt2]);
//console.log("pta,ptb", [pta, ptb]);
if (pta && ptb) {
const [xa, ya] = pta;
const [xb, yb] = ptb;
const t = ([x, y]) =>
Math.abs(ya - yb) < 1e-10 // Number.EPSILON is too small
? (x - xa) / (xb - xa)
: (y - ya) / (yb - ya);
const [t1, t2] = [t(pt1), t(pt2)];
const S = (pt1, pt2) => [pt1, pt2];
let seg;
//console.log([t1, t2]);
if (t1 < 0 && t2 < 0) cp.move(dx, dy);
else if (t1 > 1 && t2 > 1) cp.move(dx, dy);
else if (t1 < 0 && t2 > 1) {
cp.move(...vec(pt1, pta));
cp.draw(...vec(pta, ptb));
cp.move(...vec(ptb, pt2));
} else if (t1 < 0 && t2 >= 0) {
cp.move(...vec(pt1, pta));
cp.draw(...vec(pta, pt2));
} else if (t1 <= 1 && t2 > 1) {
cp.draw(...vec(pt1, ptb));
cp.move(...vec(ptb, pt2));
} else if (t2 < 0 && t1 > 1) {
cp.move(...vec(pt1, pta));
cp.draw(...vec(pta, ptb));
cp.move(...vec(ptb, pt2));
} else if (t2 < 0 && t1 >= 0) {
cp.draw(...vec(pt1, pta));
cp.move(...vec(pta, pt2));
} else if (t2 <= 1 && t1 > 1) {
cp.move(...vec(pt1, ptb));
cp.draw(...vec(ptb, pt2));
} else {
cp.draw(...vec(pt1, pt2));
}
} else cp.move(dx, dy);
} else cp.move(dx, dy);
[x1, y1] = [x2, y2];
}
return cp;
}
Insert cell
Insert cell
optimize = (plot) => {
const add = ([x, y], [dx, dy]) => [x + dx, y + dy];
const vec = ([x1, y1], [x2, y2]) => [x2 - x1, y2 - y1];
const zeroish = (val) => Math.abs(val) < 1e-5;
const S = Segments(1e-5);
let current = [0, 0];
const metrics = { before: { mv: 0, dr: 0 }, after: { mv: 0, dr: 0 } };
for (const { dr, mv } of plot) {
metrics.before[dr ? "dr" : "mv"] += Math.hypot(...(dr || mv).slice(0, 2));
const next = add(current, dr || mv);
if (dr && !dr[2]) S.addSegment([current, next]);
current = next;
}
const paths = S.getPaths(0.01);
const op = Plot().push();
current = [0, 0];
paths.forEach((path) => {
const v = vec(current, (current = path.shift()));
op.move(...v);
metrics.after.mv += Math.hypot(...v);
path.forEach((pt) => {
const v = vec(current, (current = pt));
op.draw(...v);
metrics.after.dr += Math.hypot(...v);
});
});
console.log(metrics);
return op.pop();
}
Insert cell
getBbox = (plot) => {
const bounds = MultiRange(2);
const stack = [];
const add = ([x, y], [dx, dy]) => [x + dx, y + dy];
let pos = [0, 0];
for (const { mv, dr, st } of plot) {
if (dr) {
bounds.expand(pos);
pos = add(pos, dr);
bounds.expand(pos);
} else if (mv) pos = add(pos, mv);
else if (st == "pu") stack.push(pos);
else pos = stack.pop();
}
const [[xmin, xmax], [ymin, ymax]] = bounds.bounds();
return { x: xmin, y: ymin, width: xmax - xmin, height: ymax - ymin };
}
Insert cell
Insert cell
toGcode = (plot) => {
const gcode = Gcode();
const { min, round, cos, sin, tan, PI, abs, floor } = Math;
const round4 = (value) => round((value + Number.EPSILON) * 1e4) / 1e4;
const add = ([x1, y1], [x2, y2]) => [x1 + x2, y1 + y2];
const XY = (x, y) => ({ X: round4(x), Y: round4(y) });
const IJ = ([dx, dy]) => ({ I: round4(dx), J: round4(dy) });
const up = () => gcode.move({ F: 10000, Z: round4(zDown - 4.5) });
const down = () => gcode.move({ F: 5000, Z: zDown });

gcode.millimeters(); // and thus speeds are in mm/minute
gcode.abs();
let [x, y] = [0, 0];
gcode.setCurrentCoords({ X: x, Y: y, Z: 0 });
let lastDown = [x, y];

for (const cmd of plot) {
const { mv, dr, st } = cmd;
if (mv) {
if (!lastDown) (lastDown = [x, y]), up();
[x, y] = add([x, y], mv);
} else if (dr) {
if (lastDown) {
// finally perform pending move
gcode.move({ F: 8000, ...XY(x, y) }); // fast when moving
down();
gcode.move({ F: 2000 }); // slow when drawing
lastDown = null;
}
const [dx, dy, curve] = dr;
if (curve == "+" || curve == "-") {
// quarter of ellipse
const [adx, ady] = [abs(dx), abs(dy)];
const deg2rad = (angle) => angle * (PI / 180);
const arc = (cx, cy, startAngle, endAngle) => {
const nbSegments = floor((adx + ady) / 2); // TODO: improve
const step = (endAngle - startAngle) / nbSegments;
[...Array(nbSegments)].forEach((_, i) => {
const angle = deg2rad(startAngle + (i + 1) * step);
[x, y] = [cx + adx * cos(angle), cy - ady * sin(angle)];
gcode.move(XY(x, y));
});
};
if (curve == "+") {
if (dx < 0 && dy < 0) arc(x + dx, y, 0, 90);
else if (dx < 0 && dy > 0) arc(x, y + dy, 90, 180);
else if (dx > 0 && dy > 0) arc(x + dx, y, 180, 270);
else if (dx > 0 && dy < 0) arc(x, y + dy, 270, 360);
else gcode.move(XY((x += dx), (y += dy)));
} else {
if (dx < 0 && dy > 0) arc(x + dx, y, 360, 270);
else if (dx < 0 && dy < 0) arc(x, y + dy, 270, 180);
else if (dx > 0 && dy < 0) arc(x + dx, y, 180, 90);
else if (dx > 0 && dy > 0) arc(x, y + dy, 90, 0);
else gcode.move(XY((x += dx), (y += dy)));
}
} else if (curve) {
// arc of circle
const r = 1 / (2 * tan((curve / 2) * (PI / 180)));
const [rx, ry] = [dx / 2 + dy * r, dy / 2 - dx * r];
const params = { ...IJ([rx, ry]), ...XY((x += dx), (y += dy)) };
(curve > 0 ? gcode.arc_cw : gcode.arc_ccw)(params);
} else gcode.move(XY((x += dx), (y += dy)));
}
}
if (!lastDown) up();
gcode.move({ F: 8000, X: 0, Y: 0, Z: 0 });

return gcode;
}
Insert cell
viewer = (dims = [width, 500]) => {
const attrs = {
fill: "none",
stroke: "#000",
"stroke-linecap": "round",
"stroke-linejoin": "round"
};
const toggle = (e) => {
const display = gcodeView.style.display;
gcodeView.style.display = display == "none" ? "block" : "none";
if (wrapper.plot) {
// update gCode viewer if plot has changed
gcodeView.firstChild.replaceWith(gcodeViewer(wrapper.plot));
wrapper.plot = null;
}
};
const plotView = html`<div/>`;
const gcodeView = html`<div style="background-color:#eee;display:none;padding:0 6px"><div/></div>`;
const button = htl.html`<div style="cursor:pointer" onclick=${toggle}>G-code</div>`;
const gcodeZone = htl.html`<div style="position:absolute;top:0">${button}${gcodeView}</div>`;
const wrapper = html`<div style="position:relative">${plotView}${gcodeZone}</div>`;
plotView.append(SVG.svg(dims, SVG.group()));
wrapper.update = (plot) => {
wrapper.plot = plot;
plotView.firstChild.firstChild.replaceWith(toSVG(plot));
return wrapper;
};
wrapper.view = (plot, options = {}) => {
wrapper.plot = plot;
if (!options.viewBox) {
let { x, y, width: w, height: h } = getBbox(plot);
if (w == 0) x -= (w = h / 10) / 2;
else if (h == 0) y -= (h = w / 10) / 2;
attrs.viewBox = SVG.viewbox(x, y, w, h);
}
const svg = SVG.svg(dims, toSVG(plot), Object.assign({}, attrs, options));
plotView.firstChild.replaceWith(zoomAndPanSvg(svg, 0.05));
return wrapper;
};
return wrapper;
}
Insert cell
plot = {
const plot = Plot();
plot.move(10, 0).draw(30, 0).draw(10, 10, "-");
plot.draw(0, 30).draw(-10, 10, "-");
plot.draw(-30, 0).draw(-10, -10, "-");
plot.draw(0, -30).draw(10, -10, "-").move(-10, 0);
return plot;
}
Insert cell
Insert cell
gcodeViewer = (plot) => {
const viewer = html`<div><div/></div>`;
const { x: xmin, y: ymin, width: w, height: h } = getBbox(plot);
const formats = {
A3: [297, 420],
A4: [210, 297],
A5: [105.5, 148.5],
"160x160": [160, 160],
C6: [110, 155]
};

// "format" parameter
const formatOptions = { label: "format", value: "A4" };
const format = Inputs.radio([...Object.keys(formats)], formatOptions);
// "orientation" parameter
const orientOptions = { label: "orientation", value: "landscape" };
const orient = Inputs.radio(["portrait", "landscape"], orientOptions);
// "margin" parameter
const marginOptions = { label: "margin", value: 20, step: 1 };
const margin = Inputs.range([0, 100], marginOptions);
// "scale" parameter
const scaleOptions = { label: "scale", value: "fit2page" };
const scaleMode = Inputs.radio(["fit2page", "custom ratio"], scaleOptions);
const scaleCustom = Inputs.text({ value: "1", width: 10 });
// "x offset" parameter
const xaligns = ["left", "center", "right"];
const xalign = Inputs.radio(xaligns, {
label: "x-alignment",
value: "center",
format: (x) =>
html`${{ left: "&#8676;", center: "&#8596", right: "&#8677;" }[x]}`
});
// "y offset" parameter
const yaligns = ["top", "center", "bottom"];
const yalign = Inputs.radio(yaligns, {
label: "y-alignment",
value: "center",
format: (x) =>
html`${{ top: "&#10514;", center: "&#8597", bottom: "&#10515;" }[x]}`
});

const update = () => {
const [W, H] = formats[format.value];
const transf = (W, H) => {
const M = margin.value;
const [WW, HH] = [W - M - M, H - M - M];
const customScale = +scaleCustom.value || 1;
const fit2page = scaleMode.value == "fit2page";
const ratio = fit2page ? Math.min(WW / w, HH / h) : customScale;
const fx = { left: 0, center: 0.5, right: 1 }[xalign.value];
const dx = fx * (WW - w * ratio) + M - xmin * ratio;
const fy = { top: 0, center: 0.5, bottom: 1 }[yalign.value];
const dy = fy * (HH - h * ratio) + M - ymin * ratio;
return translate(scale(plot, ratio), [dx, dy]);
};
const vplot = () => {
if (orient.value == "landscape") return transf(H, W);
const trfn = ([x, y]) => [y, -x];
return translate(transform(transf(W, H), trfn), [0, W]);
};

const view = gcodeViewer_(toGcode(vplot()), { dims: [W, H], zDown });
viewer.firstChild.replaceWith(view);
};
[format, orient, margin, scaleMode, scaleCustom, xalign, yalign].forEach(
(input) => input.addEventListener("input", update)
);
update();

const scaleDiv = html`<div style="display:flex;gap:5px">${scaleMode}${scaleCustom}</div>`;
return html`${format}${orient}${margin}${scaleDiv}${xalign}${yalign}${viewer}`;
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
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