Public
Edited
Jan 19, 2023
1 fork
5 stars
Insert cell
Insert cell
html`<svg viewbox="0 0 ${width} ${height}">
<defs>
<rect id="square" fill="${colors.ac}"
x="0" y="2" width="22" height="22" />
<path d="M15,11.5 L29,11.5 L29,15.5 L15,15.5 L15,29 L11,29 L11,15.5 L-3,15.5 L-3,11.5 L11,11.5 L11,-3 L15,-3 L15,11.5 Z"
id="cross" fill="${colors.ac}"
fill-rule="nonzero"
transform="translate(13.000000, 13.000000) rotate(-45.000000) translate(-13.000000, -13.000000)"
></path>
<polygon id="triangle"
fill="${colors.ac}" points="16 0 32 26 0 26"></polygon>
</defs>
//背景
<rect x="0" y="0" width="${width}" height="${height}" fill="${colors.bg}" />
<g transform="translate(${marginX}, ${marginY})">
${showGridLines && drawGrid()}//1绘制网格,用showGridLines控制是否显示


${showNoiseSpace && drawNoise()}//2绘制噪音,用showNoiseSpace控制是否显示
${drawIsoLines()}//3绘制等值线


${placeSymbols()}//4绘制图内的图例
</g>
${placeLegend()}//5绘制图外的图例
</svg>`
Insert cell
viewof randomize = Button("🎲 Randomize")
Insert cell
viewof widthScale = Range([1, 1.5], {//长宽比系数
value: 1,
step: 0.01,
label: md`widthScale`
})
Insert cell
viewof numOfIntervals = Range([2, 12], {//等高距,
value: 6,
step: 1,
label: md`Intervals`
})
Insert cell
viewof resolution = Range([10, 200], {//resolution,清晰度,指等高线的平滑度
value: 170,
step: 1,
label: md`Resolution`
})
Insert cell
viewof frequencyValue = Range([-10, 10], {//频率,等高线地形图的范围
value: -0.75,
step: 0.125,
label: md`Frequency`
})
Insert cell
frequency = {//频率2
if (frequencyValue === 0) return 1;
if (frequencyValue < 0) return -1 / frequencyValue;

return frequencyValue;
}
Insert cell
viewof amplitude = Range([0, 1], {//振幅
value: 1,
step: 0.01,
label: md`Amplitude`
})
Insert cell
viewof margin = Range([0, 0.5], {
value: 0.125,
step: 0.125,
label: md`Margin`
})
Insert cell
viewof strokeWidth = Range([0.125, 10], {
value: 1.75,
step: 0.125,
label: md`Stroke Width`
})
Insert cell
viewof showGridLines = Toggle({ label: "Show Grid Lines", value: true })
Insert cell
viewof numOfGridLines = Range([0, 10], {
value: 6,
step: 1,
label: md`Number of Grid Lines`,
disabled: !showGridLines
})
Insert cell
viewof showNoiseSpace = Toggle({ label: "Show Noise Space", value: false })
Insert cell
html`<hr/>`
Insert cell
aspectRatio = 3//等高线地形图的比例大小,数字大,越小
Insert cell
function placeLegend() {//5 绘制图外的图例
const legendMarginX = marginX * 2 + mapSize*widthScale;
const labelLengthRange = [32, 144];
const op = 0.25;
randomize;//上面的🎲生成的随机种子
return `<g transform="translate(${legendMarginX}, ${marginY})">
<use x="-2"
y="0"
xlink:href="#triangle"
transform="scale(0.75)"/>

<rect x="42" y="4"
//random.rangeFloor(min, max),在范围内生成随机整数
width="${CSRandom.rangeFloor(...labelLengthRange)}"
height="12" fill="${colors.fg}" opacity="${op}"/>

<use x="0"
y="48"
xlink:href="#cross"
transform="scale(0.75)"/>

<rect x="42" y="40"
width="${CSRandom.rangeFloor(...labelLengthRange)}"
height="12" fill="${colors.fg}" opacity="${op}"/>

<use x="2"
y="96"
xlink:href="#square"
transform="scale(0.75)"/>

<rect x="42" y="76"
width="${CSRandom.rangeFloor(...labelLengthRange)}"
height="12" fill="${colors.fg}" opacity="${op}"/>
</g>`;
}
Insert cell
function drawNoise() {//2 绘制噪音
let dotEls = "";
const cellSize = mapSize / (resolution - 1);

const r = 200 / resolution;
noiseSpace.flat().forEach((p) => {
const [u, v, n] = p;
const cx = CSMath.lerp(0, mapSize, u);//lerp(min, max, t),使用参数t在min和max之间进行线性插值,
//其中t通常预计在0-1之间。
//没有任何输入或输出是固定的。
const cy = CSMath.lerp(0, mapSize, v);
const o = CSMath.mapRange(n, bounds.lower, bounds.upper, 0, 1);
//mapRange(value, inputMin, inputMax, ouptutMin, outputMax, clamp = false)
//将值从一个范围“[输入最小值..输入最大值]”映射到另一个范围“[输出最小值..输出最大值]”,最小值/最大值包括在内
//默认情况下,值不是固定的,但是您可以将“clamp”指定为true,以固定“outputMin”和“outputMax”内的输出。
dotEls += `<rect x="${cx - cellSize / 2}"
y="${cy - cellSize / 2}"
width="${cellSize}"
height="${cellSize}"
fill="${colors.fg}"
opacity=${o}
/>`;
});
return dotEls;
}
Insert cell
function drawIsoLines() {//3 绘制等值线,使用了bands函数返回的值
let paths = "";
bands.forEach((d, idx) => {
paths += `<path
key="${idx}"
d="${d}z"
fill="none"
stroke="${colors.fg}"
stroke-width="${strokeWidth}"
stroke-linejoin="round"
/>`;
});

return paths;
}
Insert cell
function drawGrid() {//1 绘制网格线
const opacity = 0.5;
const gridStrokeWidth = Math.min(Math.max(0.25, strokeWidth / 2), 1);
const dashArray = "2 3";

let gridLines = "";

// Horizontal Gridlines 水平网格线
CSMath.linspace(numOfGridLines).forEach((v) => {
const y = CSMath.lerp(0, mapSize, v);//lerp(min, max, t),使用参数t在min和max之间进行线性插值,
//其中t通常预计在0-1之间。
//没有任何输入或输出是固定的。
gridLines += `<line x1="0" y1="${y}"
x2="${mapSize*widthScale}" y2="${y}"
stroke="${colors.fg}"
stroke-width="${gridStrokeWidth}"
stroke-dasharray="${dashArray}"
opacity="${opacity}" />`;
});

// Vertical Gridlines 垂直网格线
CSMath.linspace(numOfGridLines).forEach((u) => {
const x = CSMath.lerp(0, mapSize*widthScale, u);//lerp(min, max, t),使用参数t在min和max之间进行线性插值,
//其中t通常预计在0-1之间。
//没有任何输入或输出是固定的。
gridLines += `<line x1="${x}" y1="0"
x2="${x}" y2="${mapSize}"
stroke="${colors.fg}"
stroke-width="${gridStrokeWidth}"
stroke-dasharray="${dashArray}"
opacity="${opacity}" />`;
});

return gridLines;
}
Insert cell
function placeSymbols() {//4 绘制图中的图例符号
const markerMargin = mapSize / 20;
randomize;
return `<g>
<use x="${CSRandom.range(markerMargin, mapSize - markerMargin)}"
y="${CSRandom.range(markerMargin, mapSize - markerMargin)}"
xlink:href="#triangle"
transform="scale(0.75)"/>

<use x="${CSRandom.range(markerMargin, mapSize - markerMargin)}"
y="${CSRandom.range(markerMargin, mapSize - markerMargin)}"
xlink:href="#cross"
transform="scale(0.75)"/>

<use x="${CSRandom.range(markerMargin, mapSize - markerMargin)}"
y="${CSRandom.range(markerMargin, mapSize - markerMargin)}"
xlink:href="#square"
transform="scale(0.75)"/>
</g>`;
}
Insert cell
a=width
Insert cell
height = Math.floor(width / aspectRatio) //aspectRatio是比例
Insert cell
squareSize = Math.min(height, width)//squareSize,方形尺寸
Insert cell
mapSize = squareSize - squareSize * margin - strokeWidth//地图大小,方形尺寸-方形尺寸*边距占的百分比-边框宽度
Insert cell
marginX = marginY + strokeWidth / 2
Insert cell
marginY = (height - mapSize) / 2 //垂直边距=(高度-地图高度)/2
Insert cell
colors = ({
bg: "#fef4e0",
fg: "#486b8b",
ac: "#e25532"
})
Insert cell
noiseSpace = CSMath.linspace(resolution, true).map((v) =>//噪音空间,noiseSpace,生成三维数组,
//linspace(N, inclusive = false)
//在一个数组中生成一个由*N*个数字组成的线性间隔数组,从0向1插值。默认情况下,1是独占的,但您可以将 //“inclusive”传递为true,以插值到并包含1作为最终元素。
CSMath.linspace(resolution*widthScale, true).map((u) => {//random.noise3D(x, y, z, frequency = 1, amplitude = 1)
//使用“单纯形噪声”模块生成三维随机单纯形噪声。
//也可以指定噪声信号的“frequency(频率)”(将所有坐标乘以该值)和“amplitude(振幅)”(将输出结果乘以该值)。
const n = CSRandom.noise3D(u, v, randomize, frequency, amplitude);
return [u, v, n * 0.5 + 0.5];
})
)
Insert cell
noiseMap = noiseSpace.map((p) => p.map((attr) => attr[2]))//噪音贴图,noiseMap,使用了noiseSpace函数生成的数据,转化为二维数组
Insert cell
bounds = {//bounds,边界,使用了noiseMap函数生成的数据
//限制范围,输出一个对象bounds,有两个属性,
const noises = noiseMap.flat();//数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维数组。
const lower = Math.min(...noises);
const upper = Math.max(...noises);

return {
lower,
upper
};
}
Insert cell
bands生成的数组的元素的内容:<br>M 0.365237 28.0362 L 0 27.5188 L 0 28.0362 L 0 29.5118 L 0 30.9874 L 0 32.463 L 0 33.9386 L 0 35.4142 L 0 36.8898 L 0 38.3654 L 0 39.841 L 0 41.3166 L 0 42.7922 L 0 44.2677 L 0 45.7433 L 0 47.2189 L 0 48.6945 L 0 50.1701 L 0 51.6457 L 0 53.1213 L 0 54.5969 L 0 56.0725 L 0 57.5481 L 0 59.0237 L 0 60.4993 L 0 61.9749 L 0 63.4504 L 0 64.926 L 0 66.4016 L 0 67.8772 L 0 69.3528 L 0 70.8284 L 0 70.8873 L 0.126716 70.8284 L 1.47559 70.14 L 2.76134 69.3528 L 2.95118 69.2233 L 4.42677 68.0661 L 4.63839 67.8772 L 5.90237 66.5884 L 6.06558 66.4016 L 7.17952 64.926 L 7.37796 64.6151 L 8.05619 63.4504 L 8.75221 61.9749 L 8.85355 61.7108 L 9.28561 60.4993 L 9.68724 59.0237 L 9.97294 57.5481 L 10.1536 56.0725 L 10.2382 54.5969 L 10.2345 53.1213 L 10.1487 51.6457 L 9.98645 50.1701 L 9.75233 48.6945 L 9.45037 47.2189 L 9.08405 45.7433 L 8.85355 44.9568 L 8.65442 44.2677 L 8.16388 42.7922 L 7.61748 41.3166 L 7.37796 40.735 L 7.01383 39.841 L 6.35658 38.3654 L 5.90237 37.4231 L 5.6475 36.8898 L 4.88656 35.4142 L 4.42677 34.5786 L 4.07675 33.9386 L 3.21891 32.463 L 2.95118 32.0284 L 2.31246 30.9874 L 1.47559 29.6873 L 1.36289 29.5118 L 0.365237 28.0362 Z
Insert cell
intervals2 = CSMath.linspace(numOfIntervals);//间隔
Insert cell
Insert cell
bands2 = {//bands,分等级,使用了bounds函数返回的数据
const intervals = CSMath.linspace(numOfIntervals);//numOfIntervals=6,生成间隔,其实可以理解成等高距,linspace(N, inclusive = false)
//在一个数组中生成一个由*N*个数字组成的线性间隔数组,从0向1插值。默认情况下,1是独占的,但您可以将 //“inclusive”传递为true,以插值到并包含1作为最终元素。
//intervals = [0,0.16666666666666666,0.3333333333333333,0.5,0.6666666666666666,0.8333333333333334]
const isoLines = intervals.map((weight, idx) => {
//当索引大于0时,生成一条等高线,所以一共生成了5条
if (idx > 0) {
const min = intervals[idx - 1];
const max = intervals[idx];
//把值映射到和noiseMap相匹配的刻度上
const lowerBound = CSMath.mapRange(//mapRange(value, inputMin, inputMax, ouptutMin, outputMax, clamp = false)
//将值从一个范围“[输入最小值..输入最大值]”映射到另一个范围“[输出最小值..输出最大值]”,最小值/最大值包括在内
//默认情况下,值不是固定的,但是您可以将“clamp”指定为true,以固定“outputMin”和“outputMax”内的输出。
min,
0,
1,
bounds.lower,
bounds.upper
);
//把值映射到和noiseMap相匹配的刻度上
const upperBound = CSMath.mapRange(
max,
0,
1,
bounds.lower,
bounds.upper
);
//生成等值线的数组,但是二维数组的x,y值是根据数组维度生成的,需要映射到svg的高和宽
const band = MarchingSquares.isoBands(//这里用了
noiseMap,
lowerBound,
upperBound - lowerBound
);
//把值映射到svg的高和宽
return band.map((p) =>
p.map(([x, y]) => [
CSMath.mapRange(x, 0, resolution - 1, 0, mapSize),
CSMath.mapRange(y, 0, resolution - 1, 0, mapSize)
])
);
}
})
.filter((arr) => arr);//.filter((arr) => arr) 是 JavaScript 中的一种常用的数组过滤方法。它会对数组中的每一项运行给定的函数(在这里是 (arr) => arr),并将函数返回值为 true 的项组成一个新数组返回。因为 (arr) => arr 会返回其输入值本身,所以这个过滤器将会移除数组中所有的 falsy 值,并保留所有 truthy 值。

return isoLines
}
Insert cell
bands = {//bands,分等级,使用了bounds函数返回的数据,这里是生成svg的path的具体value
const intervals = CSMath.linspace(numOfIntervals);//生成间隔,其实可以理解成等高距,linspace(N, inclusive = false)
//在一个数组中生成一个由*N*个数字组成的线性间隔数组,从0向1插值。默认情况下,1是独占的,但您可以将 //“inclusive”传递为true,以插值到并包含1作为最终元素。

const isoLines = intervals.map((weight, idx) => {
//每次生成一条等高线
if (idx > 0) {
const min = intervals[idx - 1];
const max = intervals[idx];

const lowerBound = CSMath.mapRange(//mapRange(value, inputMin, inputMax, ouptutMin, outputMax, clamp = false)
//将值从一个范围“[输入最小值..输入最大值]”映射到另一个范围“[输出最小值..输出最大值]”,最小值/最大值包括在内
//默认情况下,值不是固定的,但是您可以将“clamp”指定为true,以固定“outputMin”和“outputMax”内的输出。
min,
0,
1,
bounds.lower,
bounds.upper
);
const upperBound = CSMath.mapRange(
max,
0,
1,
bounds.lower,
bounds.upper
);

const band = MarchingSquares.isoBands(//这里用了
noiseMap,
lowerBound,
upperBound - lowerBound
);

return band.map((p) =>
p.map(([x, y]) => [
CSMath.mapRange(x, 0, resolution - 1, 0, mapSize),
CSMath.mapRange(y, 0, resolution - 1, 0, mapSize)
])
);
}
})
.filter((arr) => arr);//.filter((arr) => arr) 是 JavaScript 中的一种常用的数组过滤方法。它会对数组中的每一项运行给定的函数(在这里是 (arr) => arr),并将函数返回值为 true 的项组成一个新数组返回。因为 (arr) => arr 会返回其输入值本身,所以这个过滤器将会移除数组中所有的 falsy 值,并保留所有 truthy 值。

return CSPenplot.pathsToSVGPaths(isoLines, { mapSize, mapSize, units: "px" });//参数:数据,高度,宽度,单位
}
Insert cell
md`## Imports`
Insert cell
CSUtil = require('https://bundle.run/canvas-sketch-util@1.10.0')
//https://github.com/mattdesl/canvas-sketch-util
Insert cell
CSRandom = CSUtil.random//随机
Insert cell
CSMath = CSUtil.math//数学公式
Insert cell
MarchingSquares = import(//Marching squares是计算机图形学中一个用于矩阵网格点数据生成等值面的一个算法。
//https://blog.csdn.net/whuawell/article/details/74998280
//https://github.com/RaumZeit/MarchingSquares.js
"https://unpkg.com/marchingsquares@1.3.3/dist/marchingsquares-esm.js?module"
)
Insert cell
CSPenplot = CSUtil.penplot//画笔
Insert cell
import { Toggle, Range, Button } from "@observablehq/inputs"
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