案例
折線圖適合展示隨著時間推進舒裤,數(shù)值的變化趨勢。下圖是幾家科技公司在2009年6月到2019年6月間的股票價格圖觉吭,數(shù)據(jù)來源于雅虎金融腾供。
stock-price.png
解析
SVG 有幾種元素都可以畫出線段。
line: 用于繪制直線鲜滩,只需要起始和結(jié)束點的x軸和y軸坐標就可以確定一段直線的位置伴鳖。類似這樣
<line x1="0" y1="0" x1="100" y="100" stroke="black" />
polyline: 折線元素,需要起始和結(jié)束以及中間各點的坐標來定位線段徙硅。例子像這樣:
<polyline points="0,0 20,30 50,40" fill="stroke" stroke="black"/>
path: 可以渲染各種各樣的圖形榜聂,d3.line 可以幫助我們生成 d 屬性用于描述折線。
在上面的折線例子中嗓蘑,縱坐標代表股票價格须肆,可以使用 scaleLinear 來 scale 值到圖形高度。橫坐標代表連續(xù)的時間點桩皿,每個刻度(tick)代表一天豌汇,scaleTime 正好適合這種場景。
需要注意的是泄隔,有的時間坐標場景并不適合使用 scaleTime拒贱。如果橫坐標代表不同的月份,由于每個月份的天數(shù)不一樣,那么每個月份刻度之間的距離會不同逻澳,這可能并不是我們期望的結(jié)果闸天。如果希望月份之間距離相同,那么還是采用 scalePoint 比較合適斜做。
實現(xiàn)
慣例先上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>