Published
Edited
Mar 11, 2021
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
const div = document.createElement('div');
div.innerHTML = 'Hello, World!';
return div;
}
Insert cell
{
const div = d3.create('div');
div.html('Hello, World!');
return div.node();
}
Insert cell
Insert cell
md`---
## HTML 版本`
Insert cell
md`---
### 手动绘制`
Insert cell
html`<div style="font: 10px sans-serif; text-align: right; color: white;">
<div style="background: steelblue; padding: 3px; margin: 1px; width: 40px;">4</div>
<div style="background: steelblue; padding: 3px; margin: 1px; width: 80px;">8</div>
<div style="background: steelblue; padding: 3px; margin: 1px; width: 150px;">15</div>
<div style="background: steelblue; padding: 3px; margin: 1px; width: 160px;">16</div>
<div style="background: steelblue; padding: 3px; margin: 1px; width: 230px;">23</div>
<div style="background: steelblue; padding: 3px; margin: 1px; width: 420px;">42</div>
</div>`
Insert cell
md`---
### 自动绘制`
Insert cell
{
const chart = d3.create("div")
.style("font", "10px sans-serif")
.style("text-align", "right")
.style("color", "white");

// Define the initial (empty) selection for the bars.
chart.selectAll("div")
// Bind this selection to the data (computing enter, update and exit).
.data(data)
// Join the selection and the data, appending the entering bars.
.join("div")
.style("background", "steelblue")
.style("padding", "3px")
.style("margin", "1px")
.style("width", d => `${d * 10}px`)
.text(d => d);

return chart.node();
}
Insert cell
md`---
### 自适应缩放`
Insert cell
md`上面代码的问题是,使用了一个 magic number 10 来对数据进行缩放以适应像素宽度。我们可以使用线性度量,将其转换为显式依赖。D3 的 scale 就能将抽象数据的 domain 映射到可视变量(例如位置)的 range。`
Insert cell
x = d3.scaleLinear()
.domain([0, d3.max(data)])
.range([0, 420])
Insert cell
md`返回的 x 度量是一个函数。当从 domain 中传递一个抽象数据值时,它将从 range 中返回相应的可视值。`
Insert cell
x(10)
Insert cell
x(26)
Insert cell
Insert cell
md`---
## SVG 版本`
Insert cell
md`---
### 手动绘制`
Insert cell
html`<svg width="420" height="120" font-family="sans-serif" font-size="10" text-anchor="end">
<g transform="translate(0,0)">
<rect fill="steelblue" width="40" height="19"></rect>
<text fill="white" x="37" y="9.5" dy=".35em">4</text>
</g>
<g transform="translate(0,20)">
<rect fill="steelblue" width="80" height="19"></rect>
<text fill="white" x="77" y="9.5" dy=".35em">8</text>
</g>
<g transform="translate(0,40)">
<rect fill="steelblue" width="150" height="19"></rect>
<text fill="white" x="147" y="9.5" dy=".35em">15</text>
</g>
<g transform="translate(0,60)">
<rect fill="steelblue" width="160" height="19"></rect>
<text fill="white" x="157" y="9.5" dy=".35em">16</text>
</g>
<g transform="translate(0,80)">
<rect fill="steelblue" width="230" height="19"></rect>
<text fill="white" x="227" y="9.5" dy=".35em">23</text>
</g>
<g transform="translate(0,100)">
<rect fill="steelblue" width="420" height="19"></rect>
<text fill="white" x="417" y="9.5" dy="0.35em">42</text>
</g>
</svg>`
Insert cell
md`与在HTML中使用流布局定位DIV元素不同,SVG元素必须使用绝对坐标定位:除了设置每个条的宽度,我们现在还必须设置它的垂直位置。

SVG也要求显式地定位文本。由于文本元素不支持 margin 或 padding,因此文本的位置必须从条的末端偏移3个像素,而dy偏移量用于垂直居中显示文本。`
Insert cell
md`---
### 自动绘制`
Insert cell
y = d3.scaleBand()
.domain(d3.range(data.length))
.range([0, 20 * data.length])
Insert cell
md`我们根据数据集的大小配置 y scale,通过这种方式,图表的大小是基于每个条形图的高度而不是图表的总体高度,并且我们确保有足够的空间放置标签。`
Insert cell
{
const svg = d3.create("svg")
.attr("width", width)
.attr("height", y.range()[1])
.attr("font-family", "sans-serif")
.attr("font-size", "10")
.attr("text-anchor", "end");

const bar = svg.selectAll("g")
.data(data)
.join("g")
.attr("transform", (d, i) => `translate(0,${y(i)})`);

bar.append("rect")
.attr("fill", "steelblue")
.attr("width", x)
.attr("height", y.bandwidth() - 1);

bar.append("text")
.attr("fill", "white")
.attr("x", d => x(d) - 3)
.attr("y", (y.bandwidth() - 1) / 2)
.attr("dy", "0.35em")
.text(d => d);

return svg.node();
}
Insert cell
md`虽然看起来可能不像,但这个数据集(以及结果的条形图)隐式地是二维的,第一个维度(x)是数值(4,8,15, ...);第二个维度(y)是每个值(0,1,2, ...)的索引。`
Insert cell
md`---
### 编码序数数据`
Insert cell
// 连续的,定量数据
x.domain()
Insert cell
// 序数数据
y.domain()
Insert cell
md`定量数据可以进行数值比较和运算,但是有序数据只能按次序做比较。

因此,除了定量数据的线性、pow 和 log scale 外,D3 还提供了序数数据的 scale。`
Insert cell
md`序数度量有三种形式。

在最明确的形式中,序数度量将离散的抽象数据值集(例如名称)映射到相应的离散的视觉值集(例如颜色)。
像定量度量一样,这些集分别称为 domain 和 range。`
Insert cell
z = d3.scaleOrdinal()
.domain(['apples', 'limes', 'blueberries'])
.range(['red', 'green', 'blue'])
Insert cell
z('apples')
Insert cell
z('')
Insert cell
md`在指定 domain 和 range 时,重要的是值的顺序:domain 中的元素 i 映射到 range 中的元素 i。

手动枚举指定每个 bar 的位置不太实际,我们可以使用序数度量的另一种形式:band scale。
该度量将给定的连续范围划分为均匀间隔的,均匀大小的带,如条形图所示。

最后一种形式是 point scale,用于序数点图。`
Insert cell
y(0)
Insert cell
y.bandwidth()
Insert cell
md`---
## 图表数据分离

数据通常存储在单独的文件或通过 API 获取,而不是手动编码的。将数据和图表分离,能够让代码复用和数据更新更简单。`
Insert cell
FileAttachment('numbers.csv').text()
Insert cell
md`D3 提供了 CSV parser,将文件内容转换为对象数组的形式。`
Insert cell
d3.csvParse(await FileAttachment("numbers.csv").text());
Insert cell
md`CSV 不包含类型,数据默认都是字符类型的。如果想要指定数据的类型,还需要指定一个类型转换函数。`
Insert cell
{
const data = d3.csvParse(await FileAttachment("numbers.csv").text(), d3.autoType);
const x = d3.scaleLinear()
.domain([0, d3.max(data, d => d.value)])
.range([0, width]);
const y = d3.scaleBand()
.domain(data.map(d => d.name))
.range([0, 20 * data.length]);
const svg = d3.create("svg")
.attr("width", width)
.attr("height", y.range()[1])
.attr("font-family", "sans-serif")
.attr("font-size", "10")
.attr("text-anchor", "end");

const bar = svg.selectAll("g")
.data(data)
.join("g")
.attr("transform", d => `translate(0,${y(d.name)})`);

bar.append("rect")
.attr("fill", "steelblue")
.attr("width", d => x(d.value))
.attr("height", y.bandwidth() - 1);

bar.append("text")
.attr("fill", "white")
.attr("x", d => x(d.value) - 3)
.attr("y", y.bandwidth() / 2)
.attr("dy", "0.35em")
.text(d => d.value);
return svg.node();
}
Insert cell
md`---
## 美化图表

D3 的度量通常与坐标轴配对以提高可读性。
但是在添加坐标轴之前,我们需要清除图表周围的一些空间。
按照惯例,边距被指定为一个对象,每个边都有一个属性。`
Insert cell
margin = ({top: 20, right: 0, bottom: 30, left: 40});
Insert cell
md`---
### 旋转为柱图

将条形图旋转为柱状图主要涉及到交换x和y;然而,还需要一些附带的更改。这就是直接使用SVG而不是使用高级语法(如ggplot2或Vega-Lite)的成本。`
Insert cell
md`---
### 添加坐标轴

D3的坐标轴组件将为位置度量呈现良好的、人类可读的刻度。
我们只需要提供一个scale并选择四个方向中的一个(每个方向代表图表的一面)。
在这里,我们将使用底部方向作为x轴,因为条形图从图表的底部延伸出来,而左侧方向为y轴。`
Insert cell
width
Insert cell
height = 500
Insert cell
chart = {
const data = d3.csvParse(await FileAttachment("alphabet.csv").text(), d3.autoType);
const y = d3.scaleLinear()
.domain([0, d3.max(data, d => d.frequency)])
.range([height - margin.bottom, margin.top]);
const x = d3.scaleBand()
.domain(data.map(d => d.letter))
.rangeRound([margin.left, width - margin.right])
.padding(0.1);
const yTitle = g => g.append("text")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("y", 10)
.text("↑ Frequency");
const yAxis = g => g
.attr("transform", `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(null, "%"))
.call(g => g.select(".domain").remove());
const xAxis = g => g
.attr("transform", `translate(0,${height - margin.bottom})`)
.call(d3.axisBottom(x).tickSizeOuter(0));
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append('g')
.attr('fill', 'steelblue')
.selectAll('rect')
.data(data)
.join('rect')
.attr('x', d => x(d.letter))
.attr('y', d => y(d.frequency))
.attr('height', d => y(0) - y(d.frequency))
.attr('width', x.bandwidth());
svg.append("g")
.call(xAxis)

svg.append("g")
.call(yAxis);
svg.call(yTitle);

return svg.node();
}
Insert cell
Insert cell
d3 = require('d3@6')
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