案例
折线图适合展示随着时间推进,数值的变化趋势。下图是几家科技公司在2009年6月到2019年6月间的股票价格图,数据来源于雅虎金融。
stock-price.png
解析
SVG 有几种元素都可以画出线段。
line: 用于绘制直线,只需要起始和结束点的x轴和y轴坐标就可以确定一段直线的位置。类似这样
<line x1="0" y1="0" x1="100" y="100" stroke="black" />
polyline: 折线元素,需要起始和结束以及中间各点的坐标来定位线段。例子像这样:
<polyline points="0,0 20,30 50,40" fill="stroke" stroke="black"/>
path: 可以渲染各种各样的图形,d3.line 可以帮助我们生成 d 属性用于描述折线。
在上面的折线例子中,纵坐标代表股票价格,可以使用 scaleLinear 来 scale 值到图形高度。横坐标代表连续的时间点,每个刻度(tick)代表一天,scaleTime 正好适合这种场景。
需要注意的是,有的时间坐标场景并不适合使用 scaleTime。如果横坐标代表不同的月份,由于每个月份的天数不一样,那么每个月份刻度之间的距离会不同,这可能并不是我们期望的结果。如果希望月份之间距离相同,那么还是采用 scalePoint 比较合适。
实现
惯例先上Git地址。
核心代码就几行
const xScale = d3.scaleTime()
.domain([firstDate, lastDate])
.range([0, maxWidth]);
const yScale = d3.scaleLinear()
.domain([minY, maxY])
.range([maxHeight, 0]);
const line = d3.line().x((d) => xScale(d)).y((d) => yScale(d));
d3.csv() 可以读取 csv 文件,因为这次需要读取几个比较大的文件,采用了并行读取的方式。
下面是完整版:
<!DOCTYPE html>
<html>
<body>
<style>
svg {
border: 1px solid lightgrey;
}
</style>
<div>
Stock Price of Tech Companies in the recent 10 years
</div>
<script src="http://d3js.org/d3.v5.min.js"></script>
<script type="text/javascript">
const maxHeight = 400;
const maxWidth = 600;
const barWidth = 20;
const svg = d3.select('body')
.append('svg')
.attr('width', maxWidth + 50)
.attr('height', maxHeight + 80);
const colorArray = ['#38CCCB', '#0074D9', '#2FCC40', '#FEDC00', '#FF4036'];
function renderLines(data, legends) {
const getX = (d) => d.date;
const getY = (d) => d.close;
const lineMin = (data) => d3.min(data, getY);
const lineMax = (data) => d3.max(data, getY);
const xScale = d3.scaleTime()
.domain(d3.extent(data[0], getX))
.range([0, maxWidth]);
const minY = d3.min(data, lineMin);
const maxY = d3.max(data, lineMax);
const yScale = d3.scaleLinear()
.domain([minY, maxY])
.range([maxHeight, 0]);
const line = d3.line().x((d) => xScale(getX(d))).y((d) => yScale(getY(d)))
svg.selectAll('path')
.data(data)
.enter()
.append('path')
.attr('d', (d) => {
const lineData = line(d);
return lineData;
})
.attr('stroke', (d, i) => colorArray[i % colorArray.length])
.attr('fill', 'none');
const axisRight = d3.axisRight(yScale);
svg.append('g')
.attr('transform', `translate(${maxWidth}, 0)`)
.call(axisRight);
const axisBottom = d3.axisBottom(xScale);
svg.append('g')
.attr('transform', `translate(0, ${maxHeight})`)
.call(axisBottom);
svg.append('g')
.attr('width', 500)
.attr('height', 30)
.selectAll('.legend')
.data(legends)
.enter()
.append('text')
.attr('class', 'legend')
.text((d) => d)
.attr('y', maxHeight + 50)
.attr('x', (d, i) => 50 + i * 100)
.attr('stroke', (d, i) => colorArray[i % colorArray.length])
}
async function getData(fileLocation) {
const data = await d3.csv(fileLocation, (row) => {
return {
date: new Date(row.Date),
close: parseFloat(row.Close),
};
});
return data;
}
async function render() {
const files = ['AAPL.csv', 'INTC.csv', 'FB.csv', 'AMZN.csv', 'GOOG.csv']; // stock price in the recent 10 years
const legends = files.map((file) => {
const organization = /[a-zA-Z0-9]+(?=\.csv)/;
const searchRes = organization.exec(file);
return searchRes ? searchRes[0] : '';
});
const fetchQueue = files.map(getData);
Promise.all(fetchQueue).then((data) => renderLines(data, legends));
}
render();
</script>
</body>
</html>