Public
Edited
Jan 15, 2024
6 stars
A Julia set on the Riemann sphereThe Z-CurveBarnsley's fernA stochastic digraph IFS algorithmSelf-affine tilesThe TwindragonThe Eisenstein fractionsA self-affine tile with holesSelf-affine tiles via polygon mergeGolden rectangle fractalsBifurcation diagram with critical curvesThe tame twindragonIllustrations for the proof of Green's theoremNon-orientability of a Mobius stripExamples of parametric surfacesPenrose tilingThe extended unit circlePenrose three coloringNewtons's method on the Riemann sphereConic sectionsDivisor graphsThe dance of Earth and VenusIterating multiples of the sine functionBorderline fractalsSelf-similar intersectionsBox-counting dimension examplesMandelbrot by dimensionInverse iteration for quadratic Julia setsInteger Apollonian Packings
Illustrations of two-dimensonal heat flow
The logistic bifurcation locusThe eleven unfoldings of the cubeA unimodal function with fractal level curvesGreen's theorem and polygonal areaThe geometry and numerics of first order ODEsThe xxx^xxx-spindleAnimated beatsRauzy FractalsHilbert's coordinate functionsPluckNot PiDrum strikeThe Koch snowflakeFractalized squareA Taylor series about π/4\pi/4π/4PlotX3D HyperboloidA PlotX3D animationModular arithmetic in 5th grade artSimple S-I-R ModelThe Poisson KernelPoly-gasketsClassification of 2D linear systems via trace and determinantJulia sets and the Mandelbrot setWater wavesFourier SeriesDisks for a solid of revolutionOrbit detection for the Mandelbrot setTracing a path on a spherePlot for mathematiciansFunctions of two variablesPartial derivativesDijkstra's algorithm on an RGGGradient ascentUnfolding polyhedraTangent plane to a level surfaceA strange discontinuityExamples of level surfacesMcMullen carpetsHills and valleysThe definition of ⇒Double and iterated integralsMST in an RGGTrees are bipartiteFractal typesettingd3.hierarchy and d3.treeK23 is PlanarPolar CoordinatesParametric region generatorParametric Plot 2DContour plotsGreedy graph coloringGraph6A few hundred interesting graphsThe Kings ProblemFirst order, autonomous systems of ODEsRunge-Kutta for systems of ODEs
Also listed in…
Teaching PDEs
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Gotta turn off the animation from multiple spots.
global = ({ interval_id: 0 })
Insert cell
initial_setup = {
if (solution_data && solution_data.success) {
let container = d3.select(animated_solution);
container.selectAll('.solution').remove();
let div = container
.append('div')
.attr('class', 'solution')
.style('width', scales.w.toString() + 'px')
.style('height', scales.h.toString() + 'px')
.style('position', 'relative');

let scene = div
.append('x3d')
// .attr('disableKeys', 'true')
.attr('width', scales.w.toString() + 'px')
.attr('height', scales.h.toString() + 'px')
.append('scene');

scene
.append('orthoviewpoint')
.attr('id', 'ortho_viewpoint')
.attr(
'position',
`${scales.xcenter} ${scales.ycenter} ${2 * scales.range}`
)
.attr('centerOfRotation', `${scales.xcenter} ${scales.ycenter} 0`)
.attr(
'fieldOfView',
`[${scales.xmin}, ${scales.ymin}, ${scales.xmax}, ${scales.ymax}]`
);
scene
.append('viewpoint')
.attr('id', 'classic_viewpoint')
.attr(
'position',
`${scales.xcenter + 1.8 * scales.range * 0.894427} ${
scales.ycenter
} ${1.8 * scales.range * 0.447214}`
)
.attr('orientation', '0.465341 0.465341 0.752938 1.85084')
.attr('centerOfRotation', `${scales.xcenter} ${scales.ycenter} 0`);

let coord_string = solution_data.coords
.map((p, i) => [p[0], p[1], solution_data.value_series[0][i]])
.toString()
.replace(/,/g, ' ');
let index_string = solution_data.cells
.map(t => [t[0], t[1], t[2], -1])
.toString()
.replace(/,/g, ' ');
let color_string = solution_data.value_series[0]
.map(x => rgb_to_r_g_b(d3.interpolateRdBu(scales.v_scale(x))))
.toString()
.replace(/,/g, ' ');

let graph = scene.append('transform').attr('scale', '1,1,0');
let faces = graph.append('shape');
let ifs = faces
.append('indexedFaceSet')
.attr('coordIndex', index_string)
.attr('solid', false);
ifs.append('Coordinate').attr('point', coord_string);
ifs.append('Color').attr('color', color_string);

let dv = (scales.max_value - scales.min_value) / 8;
let segments = d3
.range(scales.min_value + dv / 2, scales.max_value, dv)
.map(z =>
solution_data.cells
.map(t => build_contours(t, z, 0, solution_data))
.filter(x => x != undefined)
)
.flat(2);
graph.append(() => create_indexedLineSet(segments));

let coordinate_overlay = div
.append('div')
.attr('class', 'overlay')
.style('width', scales.w.toString() + 'px')
.style('height', scales.h.toString() + 'px')
.style('position', 'absolute')
.style('top', '0px')
.style('left', '0px');

let svg = coordinate_overlay
.append('svg')
.attr('id', 'coordinate_overlay')
.attr('width', scales.w)
.attr('height', scales.h);
let hline = svg
.append('line')
.attr('id', 'hline')
.attr('x1', 0)
.attr('x2', scales.w)
.attr('y1', 0)
.attr('y2', 0)
.attr('stroke', 'black')
.attr('opacity', 0);
let vline = svg
.append('line')
.attr('id', 'vline')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', 0)
.attr('y2', scales.h)
.attr('stroke', 'black')
.attr('opacity', 0);

coordinate_overlay
.on('mouseenter', function() {
let selected_viewpoint = d3
.select(viewpoint_selector)
.selectAll('input')
.nodes()
.filter(function(o) {
return o.checked;
})[0].value;
if (selected_viewpoint == 'twoD') {
coordinate_overlay.selectAll('line').style('opacity', 1);
}
})
.on('pointermove', function(evt) {
let anchor;
let xtext;
if (scales.w - evt.layerX < 200) {
anchor = 'end';
xtext = -5;
} else {
anchor = 'start';
xtext = 5;
}
svg
.select('#hline')
.attr('y1', `${evt.layerY}px`)
.attr('y2', `${evt.layerY}px`);
svg
.select('#vline')
.attr('x1', `${evt.layerX}px`)
.attr('x2', `${evt.layerX}px`);
let x0 = scales.x_scale.invert(evt.layerX);
let y0 = scales.y_scale.invert(evt.layerY);
let u0 = u(x0, y0, 0);
svg.selectAll('.value').remove();
if (typeof u0 == 'number') {
svg
.append('text')
.attr('class', 'value')
.style('font-style', 'italic')
.attr('x', evt.layerX + xtext)
.attr('y', evt.layerY - 5)
.attr('text-anchor', anchor)
.text(`u(${ff(x0)}, ${ff(y0)}) = ${ff(u0)}`);
}
})
.on('mouseleave', function() {
coordinate_overlay.selectAll('line').style('opacity', 0);
coordinate_overlay.selectAll('.value').remove();
});

x3dom.reload();
}
}
Insert cell
function set_solution_pic(k) {
let face_coord_string = solution_data.coords
.map((p, i) => [p[0], p[1], solution_data.value_series[k][i]])
.toString()
.replace(/,/g, ' ');
let color_string = solution_data.value_series[k]
.map(x => rgb_to_r_g_b(d3.interpolateRdBu(scales.v_scale(x))))
.toString()
.replace(/,/g, ' ');
let ifs = d3.select(animated_solution).select('indexedFaceSet');
ifs.select('Color').attr('color', color_string);
ifs.select('Coordinate').attr('point', face_coord_string);

let dv = (scales.max_value - scales.min_value) / 8;
let segments = d3
.range(scales.min_value + dv / 2, scales.max_value, dv)
.map(z =>
solution_data.cells
.map(t => build_contours(t, z, k, solution_data))
.filter(x => x != undefined)
)
.flat(2);
let index_string = path_list_to_indexString(segments);
let contour_coord_string = path_list_to_coordString(segments);
d3.select(animated_solution)
.select('indexedLineSet')
.attr('coordIndex', index_string)
.select('Coordinate')
.attr('point', contour_coord_string);

let coordinate_overlay = d3.select(animated_solution).select('.overlay');
let svg = coordinate_overlay.select('svg');
coordinate_overlay.on('pointermove', function(evt) {
let anchor;
let xtext;
if (scales.w - evt.layerX < 200) {
anchor = 'end';
xtext = -5;
} else {
anchor = 'start';
xtext = 5;
}
svg
.select('#hline')
.attr('y1', `${evt.layerY}px`)
.attr('y2', `${evt.layerY}px`);
svg
.select('#vline')
.attr('x1', `${evt.layerX}px`)
.attr('x2', `${evt.layerX}px`);
let x0 = scales.x_scale.invert(evt.layerX);
let y0 = scales.y_scale.invert(evt.layerY);
let u0 = u(x0, y0, k);
svg.selectAll('.value').remove();
if (typeof u0 == 'number') {
svg
.append('text')
.attr('class', 'value')
.style('font-style', 'italic')
.attr('x', evt.layerX + xtext)
.attr('y', evt.layerY - 5)
.attr('text-anchor', anchor)
.text(`u(${ff(x0)}, ${ff(y0)}) = ${ff(u0)}`);
}
});
}
Insert cell
// An object containing scale data that's used by both
// initial_setup and set_solution_pic
scales = {
if (solution_data && solution_data.success) {
let xs = solution_data.coords.map((p) => p[0]);
let xmin = d3.min(xs);
let xmax = d3.max(xs);
let xcenter = (xmin + xmax) / 2;
let xrange = xmax - xmin;
let ys = solution_data.coords.map((p) => p[1]);
let ymin = d3.min(ys);
let ymax = d3.max(ys);
let ycenter = (ymin + ymax) / 2;
let yrange = ymax - ymin;
let aspect = yrange / xrange;
let range = d3.max([xrange, yrange]);

let w = width < 800 ? width : 800; // 0.8 * width;
w = w - 24;
let h = aspect * w;

let x_scale = d3.scaleLinear().domain([xmin, xmax]).range([0, w]);
let y_scale = d3.scaleLinear().domain([ymin, ymax]).range([h, 0]);

let min_value = d3.min(solution_data.value_series.flat());
let max_value = d3.max(solution_data.value_series.flat());
let v_scale = d3.scaleLinear().domain([max_value, min_value]).range([0, 1]);

return {
w: w,
h: h,
xmin: xmin,
xmax: xmax,
xrange: xrange,
xcenter: xcenter,
ymin: ymin,
ymax: ymax,
yrange: yrange,
ycenter: ycenter,
range: range,
aspect: aspect,
min_value: min_value,
max_value: max_value,
x_scale: x_scale,
y_scale: y_scale,
v_scale: v_scale
};
}
}
Insert cell
// // The actual solution used to display the value on hover over 2D
function u(x, y, k) {
let cc = solution_data.coords;
let T0 = solution_data.cells.filter(T =>
barycentric_coords([x, y], [cc[T[0]], cc[T[1]], cc[T[2]]])
);
if (T0.length > 0) {
T0 = T0[0];
let bcc = barycentric_coords([x, y], [cc[T0[0]], cc[T0[1]], cc[T0[2]]]);
let vv = solution_data.value_series[k];
return vv[T0[0]] * bcc[0] + vv[T0[1]] * bcc[1] + vv[T0[2]] * bcc[2];
}
}
Insert cell
function build_contours([a, b, c], z, k, region_data) {
let contours = [];
let i1 = find_intersection(a, b, z, k, region_data);
let i2 = find_intersection(b, c, z, k, region_data);
let i3 = find_intersection(c, a, z, k, region_data);
if (i1 && i2) {
contours.push([i1, i2]);
}
if (i2 && i3) {
contours.push([i2, i3]);
}
if (i3 && i1) {
contours.push([i3, i1]);
}
if (contours.length > 0) {
return contours;
}
}
Insert cell
// a and b should be integers specifying a segment in cells.
// z is a real number specifying a contour value.
// Output should be a point on the segment where the contour crosses,
// together with the solution value there.
function find_intersection(a, b, z, k, region_data) {
let fa = region_data.value_series[k][a];
let fb = region_data.value_series[k][b];
let ax, ay, bx, by;
if ((fa < z && z < fb) || (fb < z && z < fa)) {
let s = (z - fa) / (fb - fa);
[ax, ay] = region_data.coords[a];
[bx, by] = region_data.coords[b];
let cx = ax + (bx - ax) * s;
let cy = ay + (by - ay) * s;
let cz = fa + (fb - fa) * s;
return [cx, cy, cz];
}
}
Insert cell
function rgb_to_r_g_b(rgb_string) {
return String(
rgb_string
.split('(')[1]
.split(')')[0]
.split(',')
.map(x => String(parseFloat(x) / 255))
).replace(/,/g, ' ');
}
Insert cell
// Used to interpolate solution throughout a cell
function barycentric_coords(p, T) {
let x = p[0];
let y = p[1];
let x1 = T[0][0];
let y1 = T[0][1];
let x2 = T[1][0];
let y2 = T[1][1];
let x3 = T[2][0];
let y3 = T[2][1];
let B = (x1 - x3) * (y2 - y3) - (x2 - x3) * (y1 - y3);
if (Math.abs(B) == 0) {
return false;
}
let lambda1 = ((y2 - y3) * (x - x3) + (x3 - x2) * (y - y3)) / B;
let lambda2 = ((y3 - y1) * (x - x3) + (x1 - x3) * (y - y3)) / B;
let lambda3 = 1 - lambda1 - lambda2;

if (lambda1 > 0 && lambda2 > 0 && lambda3 > 0) {
return [lambda1, lambda2, lambda3];
} else {
return false;
}
}
Insert cell
// Precomputed solutions

solution_data = {
let solution_data;
if (example == "bar") {
solution_data = await FileAttachment("bar@8.json").json();
} else if (example == "bar_with_insulated_hole") {
solution_data = await FileAttachment(
"bar_with_insulated_hole@4.json"
).json();
} else if (example == "bar_with_fixed_hole") {
solution_data = await FileAttachment("bar_with_fixed_hole.json").json();
} else if (example == "punctured_disk") {
solution_data = await FileAttachment("punctured_disk@3.json").json();
} else if (example == "annulus") {
solution_data = await FileAttachment("annulus@3.json").json();
} else if (example == "relaxed_pringle") {
solution_data = await FileAttachment("relaxed_pringle@6.json").json();
} else if (example == "kidney") {
solution_data = await FileAttachment("kidney_heat@10.json").json();
solution_data.coords = solution_data.coords.map(a => a.map(parseFloat));
solution_data.value_series = solution_data.value_series.map(a =>
a.map(parseFloat)
);
// solution_data.success = true;
}
return solution_data;
}
Insert cell
commentary = ({
bar: `In the simplest example, we have a metal bar whose inital temperature distribution is cold throughout. The sides of the bar (top and bottom in the picture) are insulated but the ends are exposed. At time zero, the right end is brought into contact with a hot stove while the left remains in contact with a block of ice - effectively fixing the temperatures at the endpoints. We expect the heat to conduct from right to left until a uniform steady state is reached. Since the there is very little lateral variation, this is an essentially one-dimensional problem.

Hit the start button below to visualize the process.`,
bar_with_insulated_hole:
"This is a lot like the simple bar but there's an insulated hole obstructing the heat flow. In addition, the boundary condition on the right is non-constant, which is easy to see in 3D. We again expect to see the heat flow from right to left but we also expect the hole to influence the flow.",
bar_with_fixed_hole:
"This is a lot like the bar with insulated hole, but now the sides of the hole are fixed to an intermediate temperature. I guess the resulting flow should be similar to the last example but the hole ought to impeed the flow a bit more.",
punctured_disk:
"We start with a disk that is uniformly cool throughout. At time zero, we poke out the center with a red hot poker leaving it there so that the edge of the small hole is set to a high temperature. We expect the heat to diffuse radially.",
relaxed_pringle: `In this example, we start with a disk whose temperature varies throughout and has a maximum near the middle. At time zero, we hold the boundary constant and allow the heat to diffuse. The heat diffuses away from the maximum and, in the steady state, the temperature at the center is something like the average.`,
annulus: `We have an annulus whose inner radius is 1/5 the size of the outer radius. At time zero, the temperature is zero throughout and both circular boundaries have a temperature that is fixed to zero, perhaps by a refrigerator coil. In the absence of any external influence, the temperature should remain zero throughout but in this problem, we introduce an external heat source - perhaps a heat lamp. We expect the temperature to rise in the interior of the annulus while remaining cool on the boundary.`,
kidney:
"The boundaries of this final example are bound by the countours of a slightly complicated function. It's meant to illustrate the fact that we can do all this for some fairly complicated shapes."
})
Insert cell
ff = d3.format('0.5f')
Insert cell
// interactive_url = {
// let params = new URLSearchParams(location.search);
// let locale = params.get("locale");
// if (locale == 'web') {
// return 'https://marksmath.org/visualization/HeatExplorer2D.html';
// } else {
// return 'https://observablehq.com/d/ee5f334ac049a573';
// }
// }
Insert cell
// Code for the svg illustrating the rectangle with hole.
// {
// let w = width < 600 ? width : 600;
// let h = width < 600 ? width / 3 : 200;
// let svg = d3
// .create('svg')
// .attr('width', w)
// .attr('height', h);
// svg
// .append('path')
// .attr('d', d3.line()([[0, 0], [w, 0], [w, h], [0, h]]) + 'Z')
// .attr('stroke', 'black')
// .attr('stroke-width', 3)
// .attr('fill', '#eee');
// svg
// .append('circle')
// .attr('cx', (5 * w) / 6)
// .attr('cy', h / 2)
// .attr('r', 50)
// .attr('stroke', 'black')
// .attr('stroke-width', 3)
// .attr('fill', '#fff');
// svg
// .append('g')
// .attr('transform', `translate(${w / 5} ${0.42 * h}) scale(2)`)
// .append(() => MathJax.tex2svg(String.raw`R`).querySelector("svg"));
// svg
// .append('g')
// .attr('transform', `translate(${0.68 * w} ${0.17 * h}) scale(1.2)`)
// .append(() => MathJax.tex2svg(String.raw`\partial R`).querySelector("svg"));
// let arrow_group = svg.append('g');
// arrow_group // from http://thenewcode.com/1068/Making-Arrows-in-SVG
// .append('svg:defs')
// .append('marker')
// .attr('id', 'arrowhead')
// .attr("markerWidth", 10)
// .attr("markerHeight", 7)
// .attr("refX", 0)
// .attr("refY", 3.5)
// .attr("orient", "auto")
// .append('polygon')
// .attr('points', '0 0, 10 3.5, 0 7');
// arrow_group
// .append('path')
// .attr('d', d => d3.line()([[0.69 * w, 0.16 * h], [0.66 * w, 0.04 * h]]))
// .style('stroke', 'black')
// .style('stroke-width', 0.8)
// .style('fill', 'none')
// .attr("marker-end", "url(#arrowhead)");
// arrow_group
// .append('path')
// .attr('d', d => d3.line()([[0.72 * w, 0.27 * h], [0.75 * w, 0.345 * h]]))
// .style('stroke', 'black')
// .style('stroke-width', 0.8)
// .style('fill', 'none')
// .attr("marker-end", "url(#arrowhead)");
// return svg.node();
// }
Insert cell
Insert cell
import { select } from "@jashkenas/inputs"
Insert cell
import { create_indexedLineSet } from '@mcmcclur/x3dom-primitives'
Insert cell
import {
path_list_to_coordString,
path_list_to_indexString
} from 'ba6b3d20e94e294f'
Insert cell
MathJax = require('https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-svg.js').catch(
() => window['MathJax']
)
Insert cell
x3dom = require('x3dom').catch(() => window['x3dom'])
Insert cell
d3 = require('d3@6')
Insert cell
html`<style>
canvas {
outline: none
}
</style>`
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