Public
Edited
Feb 7
Insert cell
Insert cell
Insert cell
M1 = [
[4, 2],
[2, -2]
]
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
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
M4 = [
[1, 2, -1, -4],
[2, 3, -1, -11],
[-2, 0, -3, 22]
]
Insert cell
make_forward_steps(M4)
Insert cell
Insert cell
Insert cell
add_response = add_pic.update(add_step % add_pic.data.steps.length)
Insert cell
mult_response = mult_pic.update(mult_step % mult_pic.data.steps.length)
Insert cell
swap_response = {
swap_pic.update(swap_step % swap_pic.data.steps.length);
return html`<div style="display: none"></div>`;
}
Insert cell
pic1_2_response = pic1_2.update(step1_2 % pic1_2.data.steps.length)
Insert cell
pic1_response = pic1.update(step1 % pic1.data.steps.length)
Insert cell
Insert cell
Insert cell
viewof one_step = Inputs.button("Apply")
Insert cell
pic1step = make_one_step_transition_pic([
[2, 4],
[1, 2]
])
Insert cell
one_step_response = pic1step.update(one_step)
Insert cell
function make_one_step_transition_pic(M) {
let [x0, y0] = [M[0][0], M[1][0]];
let [x1, y1] = [M[0][1], M[1][1]];

let [xmin, xmax] = d3.extent([0, x0, x1, x0 + x1]);
let [ymin, ymax] = d3.extent([0, y0, y1, y0 + y1]);

let xrange = xmax - xmin;
let s = 0.4;
xmin = xmin - s * xrange;
xmax = xmax + s * xrange;
let yrange = ymax - ymin;
ymin = ymin - s * yrange;
ymax = ymax + s * yrange;

const w = 500;
const h = (w * (ymax - ymin)) / (xmax - xmin);
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.attr("width", "100%")
.style("max-width", `${w}px`)
.style("border", "solid black 2px");

const pad = 10;
// let [xmin, xmax, ymin, ymax] = [-3, 3, -1, 3];
const x_scale = d3
.scaleLinear()
.domain([xmin, xmax])
.range([pad, w - pad]);
const y_scale = d3
.scaleLinear()
.domain([ymin, ymax])
.range([h - pad, pad]);
const pointToPoint = ([x, y]) => [x_scale(x), y_scale(y)];

const configuration = svg.append("g");
const polygon = configuration
.append("polygon")
.attr(
"points",
[
[0, 0],
[1, 0],
[1, 1],
[0, 1],
[0, 0]
].map(pointToPoint)
)
.attr("fill", "lightgray")
.attr("stroke", "black")
.attr("stroke-width", 0.5);
const leg0 = configuration
.append("polyline")
.call(arrow, x_scale(0), y_scale(0), x_scale(1), y_scale(0))
.attr("stroke", "red")
.attr("stroke-width", 2);
const leg1 = configuration
.append("polyline")
.call(arrow, x_scale(0), y_scale(0), x_scale(0), y_scale(1))
.attr("stroke", "blue")
.attr("stroke-width", 2);

svg
.append("g")
.style("font-size", "14px")
.attr("transform", `translate(0, ${y_scale(0)})`)
.call(d3.axisBottom(x_scale).ticks(4));
svg
.append("g")
.style("font-size", "14px")
.attr("transform", `translate(${x_scale(0)}, 0)`)
.call(d3.axisLeft(y_scale).ticks(4));

svg.node().update = update;
return svg.node();

function update(state) {
let x00, y00, x11, y11;
if (state % 2 == 0) {
x00 = 1;
y00 = 0;
x11 = 0;
y11 = 1;
} else {
x00 = x0;
y00 = y0;
x11 = x1;
y11 = y1;
}
leg0
.transition()
.duration(850)
.attr(
"points",
arrowPoints(x_scale(0), y_scale(0), x_scale(x00), y_scale(y00))
);
leg1
.transition()
.duration(850)
.attr(
"points",
arrowPoints(x_scale(0), y_scale(0), x_scale(x11), y_scale(y11))
);

polygon
.transition()
.duration(850)
.attr(
"points",
[
[0, 0],
[x00, y00],
[x00 + x11, y00 + y11],
[x11, y11],
[0, 0]
].map(pointToPoint)
);
}
}
Insert cell
function make_transition_pic(M, back = false) {
const data = rref(M);

data.steps.reverse();
data.steps.push({
op: "",
em: [
[1, 0],
[0, 1]
],
current: M
});
if (!back) {
data.steps.reverse();
}

let [x0, y0] = [M[0][0], M[1][0]];
let [x1, y1] = [M[0][1], M[1][1]];

// Not so good an attempt to improve the range
// let pts = data.steps.map(function (o) {
// const xs = [o.current[0][0], o.current[0][1]];
// const ys = [o.current[1][0], o.current[1][1]];
// return [xs, ys];
// });
// let xs = [pts.map((pt) => pt[0]), [x0, x1]].flat(2).map((x) => 2 * x);
// let ys = [pts.map((pt) => pt[1]), [y0, y1]].flat(2).map((y) => 2 * y);
// let [xmin, xmax] = d3.extent(xs);
// let [ymin, ymax] = d3.extent(ys);

let [xmin, xmax] = d3.extent([0, x0, x1, x0 + x1]);
let [ymin, ymax] = d3.extent([0, y0, y1, y0 + y1]);

let xrange = xmax - xmin;
let s = 0.4;
xmin = xmin - s * xrange;
xmax = xmax + s * xrange;
let yrange = ymax - ymin;
ymin = ymin - s * yrange;
ymax = ymax + s * yrange;

const w = 500;
const h = (w * (ymax - ymin)) / (xmax - xmin);
const svg = d3
.create("svg")
.attr("viewBox", [0, 0, w, h])
.attr("width", "100%")
.style("max-width", `${w}px`)
.style("border", "solid black 2px");

const pad = 10;
// let [xmin, xmax, ymin, ymax] = [-3, 3, -1, 3];
const x_scale = d3
.scaleLinear()
.domain([xmin, xmax])
.range([pad, w - pad]);
const y_scale = d3
.scaleLinear()
.domain([ymin, ymax])
.range([h - pad, pad]);
const pointToPoint = ([x, y]) => [x_scale(x), y_scale(y)];

if (back) {
[x0, y0] = [1, 0];
[x1, y1] = [0, 1];
}
const configuration = svg.append("g");
const polygon = configuration
.append("polygon")
.attr(
"points",
[
[0, 0],
[x0, y0],
[x0 + x1, y0 + y1],
[x1, y1],
[0, 0]
].map(pointToPoint)
)
.attr("fill", "lightgray")
.attr("stroke", "black")
.attr("stroke-width", 0.5);
const leg0 = configuration
.append("polyline")
.call(arrow, x_scale(0), y_scale(0), x_scale(x0), y_scale(y0))
.attr("stroke", "red")
.attr("stroke-width", 2);
const leg1 = configuration
.append("polyline")
.call(arrow, x_scale(0), y_scale(0), x_scale(x1), y_scale(y1))
.attr("stroke", "blue")
.attr("stroke-width", 2);

svg
.append("g")
.style("font-size", "14px")
.attr("transform", `translate(0, ${y_scale(0)})`)
.call(d3.axisBottom(x_scale).ticks(4));
svg
.append("g")
.style("font-size", "14px")
.attr("transform", `translate(${x_scale(0)}, 0)`)
.call(d3.axisLeft(y_scale).ticks(4));

const leg_steps = data.steps.map(function (o) {
const d = o.current;
const [x0, x1] = d[0];
const [y0, y1] = d[1];
return [
[x0, y0],
[x1, y1]
];
});

function update(state) {
const [x0, y0] = leg_steps[state][0];
const [x1, y1] = leg_steps[state][1];
leg0
.transition()
.duration(850)
.attr(
"points",
arrowPoints(x_scale(0), y_scale(0), x_scale(x0), y_scale(y0))
);
leg1
.transition()
.duration(850)
.attr(
"points",
arrowPoints(x_scale(0), y_scale(0), x_scale(x1), y_scale(y1))
);

polygon
.transition()
.duration(850)
.attr(
"points",
[
[0, 0],
[x0, y0],
[x0 + x1, y0 + y1],
[x1, y1],
[0, 0]
].map(pointToPoint)
);
}

svg.node().update = update;
svg.node().data = data;
return svg.node();
}
Insert cell
// https://observablehq.com/@oberbichler/arrow
function arrow(selection, ax, ay, bx, by, options = {}) {
const points = arrowPoints(ax, ay, bx, by, options);

selection.attr("points", points).attr("stroke-linejoin", "round");

return selection;
}
Insert cell
function arrowPoints(ax, ay, bx, by, options = {}) {
const dx = bx - ax;
const dy = by - ay;

const length = Math.sqrt(dx ** 2 + dy ** 2);

const ux = dx / length;
const uy = dy / length;

const nx = -uy;
const ny = ux;

const h = options.arrowLength || 8;
const w = options.arrowWidth || 4;

const mx = bx - ux * h;
const my = by - uy * h;

const lx = mx - (nx * w) / 2;
const ly = my - (ny * w) / 2;

const rx = mx + (nx * w) / 2;
const ry = my + (ny * w) / 2;

return [ax, ay, mx, my, lx, ly, bx, by, rx, ry, mx, my];
}
Insert cell
Insert cell
function make_forward_steps(M) {
const data = rref(M);
let step_tex = math.parse(math.matrix(M).toString()).toTex();
data.steps.forEach(function (step) {
const current_tex = math
.parse(math.matrix(step.current).toString())
.toTex();
step_tex = step_tex + `&\\xrightarrow{${step.op}}&${current_tex} \\\\`;
});

return tex.block`\begin{aligned}${step_tex}\end{aligned}`;
}
Insert cell
function make_backward_steps(M) {
const data = rref(M);
let step_tex = math.parse(math.matrix(data.result).toString()).toTex();
for (let i = data.steps.length - 1; i >= 0; i--) {
const matrix_step = i > 0 ? data.steps[i - 1].current : data.init;
const op_step = data.steps[i];
const current_tex = math.parse(math.matrix(matrix_step).toString()).toTex();
step_tex =
step_tex + `&\\xrightarrow{${op_step.opInv}}&${current_tex} \\\\`;
}

return tex.block`\begin{aligned}${step_tex}\end{aligned}`;
}
Insert cell
// A mostly ChatGPT written function to compute the RREF and
// keep track of what operations were applied.
function rref(M) {
const matrix = cloneMatrix(M);
let lead = 0;
let rowCount = matrix.length;
let colCount = matrix[0].length;
let steps = [];
let init = matrix.map((row) => [...row]);

for (let r = 0; r < rowCount; r++) {
if (lead >= colCount) {
return { result: matrix, steps, init };
}
let i = r;
while (matrix[i][lead] === 0) {
i++;
if (i === rowCount) {
i = r;
lead++;
if (lead === colCount) {
return { result: matrix, steps, init };
}
}
}

if (i !== r) {
[matrix[i], matrix[r]] = [matrix[r], matrix[i]];
let em = identityMatrix(rowCount);
[em[i], em[r]] = [em[r], em[i]];
steps.push({
op: `Swap R${i + 1} <-> R${r + 1}`,
em,
emInv: inverseMatrix(em),
opInv: `Swap R${r + 1} <-> R${i + 1}`,
current: cloneMatrix(matrix)
});
}

let lv = matrix[r][lead];
if (lv !== 1) {
let em = identityMatrix(rowCount);
em[r][r] = 1 / lv;
for (let j = 0; j < colCount; j++) {
matrix[r][j] /= lv;
}
steps.push({
op: `R${r + 1} / ${lv} -> R${r + 1}`,
em,
emInv: inverseMatrix(em),
opInv: `R${r + 1} * ${lv} -> R${r + 1}`,
current: cloneMatrix(matrix)
});
}

for (let i = 0; i < rowCount; i++) {
if (i !== r) {
let lv = matrix[i][lead];
if (lv !== 0) {
let em = identityMatrix(rowCount);
em[i][r] = -lv;
for (let j = 0; j < colCount; j++) {
matrix[i][j] -= lv * matrix[r][j];
}
steps.push({
op: `${-lv}R${r + 1} + R${i + 1} -> R${i + 1}`,
em,
emInv: inverseMatrix(em),
opInv: `${lv}R${r + 1} + R${i + 1} -> R${i + 1}`,
current: cloneMatrix(matrix)
});
}
}
}
lead++;
}
return { result: matrix, steps, init };
}
Insert cell
function cloneMatrix(mat) {
return mat.map((row) => [...row]);
}
Insert cell
function identityMatrix(n) {
return Array.from({ length: n }, (_, i) =>
Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))
);
}
Insert cell
function inverseMatrix(em) {
let size = em.length;
let inv = identityMatrix(size);
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
if (i !== j) {
inv[i][j] = -em[i][j];
} else {
inv[i][j] = 1 / em[i][j];
}
}
}
return inv;
}
Insert cell
Insert cell
math = require("mathjs")
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