在經(jīng)過(guò)了一個(gè)階段的學(xué)習(xí)之后讨越,我們對(duì)基本的坐標(biāo)軸和比例尺都有了很好的了解,今天我們結(jié)合之前的內(nèi)容永毅,配合節(jié)流函數(shù)來(lái)制作一款精美的可交互折線圖把跨。
準(zhǔn)備的數(shù)據(jù)
const line_data = [
{
country: "china",
gdp: [
[2008, 2033],
[2009, 2400],
[2010, 4333],
[2011, 5600],
[2012, 6500],
[2013, 6700],
[2014, 6933],
[2015, 7400],
[2016, 7733],
[2017, 8200]
]
},
{
country: "japan",
gdp: [
[2008, 3333],
[2009, 4400],
[2010, 5233],
[2011, 5800],
[2012, 6333],
[2013, 6400],
[2014, 6533],
[2015, 6700],
[2016, 7033],
[2017, 7200]
]
}
]
添加坐標(biāo)軸
坐標(biāo)軸的建立在前幾節(jié)已多次介紹,這里就不再贅述沼死。
const data = line_data;
var initWidth = 340
var initHeight = 500
var padding = { left:40, top:10, right:20, bottom: 20}
var height = initWidth - padding.top - padding.bottom
var width = initHeight - padding.left - padding.right
var svg = d3.select("body")
.append("svg")
.attr("id", "chart")
.attr("width", width)
.attr("height", height)
.style("padding-left", padding.left)
.style("padding-right", padding.right)
.style("padding-top", padding.top)
.style("padding-bottom", padding.bottom)
//添加y軸坐標(biāo)軸
//y軸比例尺
let nums = [...data[0]["gdp"], ...data[1]["gdp"]].map(function(e){
return e[1]
})
let yScale = d3.scaleLinear()
.domain([0, d3.max(nums)])
.range([height , 0]);
let _yScale = d3.scaleLinear()
.domain([0, d3.max(nums)])
.range([0, height]);
//定義y軸
let yAxis = d3.axisLeft(yScale)
.tickFormat(d3.format("d")); //把x,xxx 的數(shù)據(jù)計(jì)數(shù)方式格式化着逐,轉(zhuǎn)化為不帶逗號(hào)的格式
//添加y軸
svg.append("g")
.attr("class","axis")
.attr("transform","translate(" + 0 + "," + 0 + ")")
.call(yAxis);
//添加x軸坐標(biāo)軸
//x軸比例尺
let years = data[0]["gdp"].map(function(e){
return e[0]
})
let xScale = d3.scaleLinear()
.domain([2008,2017])
.rangeRound([0, width])
let _xScale = d3.scaleLinear()
.domain([0,width])
.rangeRound([2008, 2017])
//定義x軸
let xAxis = d3.axisBottom(xScale)
.tickFormat(d3.format("d"))
//添加x軸
svg.append("g")
.attr("class","axis")
.attr("transform","translate(" + "0 ," + height + ")")
.call(xAxis);
坐標(biāo)軸的樣式
.axis path {
stroke: steelblue;
stroke-width: 1
}
.axis .tick line{
stroke: steelblue;
stroke-width: 3
}
添加背景間隔線
添加網(wǎng)線的內(nèi)容上一節(jié)已經(jīng)介紹過(guò)了,這次只使用了y軸方向的網(wǎng)線意蛀,來(lái)幫助使用者確立數(shù)據(jù)位置
//添加x軸
svg.append("g")
.attr("class","axis")
.attr("transform","translate(" + "0 ," + height + ")")
.call(xAxis);
//添加
// gridlines in x axis function
function make_x_gridlines() {
return d3.axisBottom(xScale)
.ticks(years.length)
}
// add the X gridlines
var grid = svg.append("g")
.attr("id", "grid")
.attr("transform", "translate(0," + height + ")")
.call(make_x_gridlines()
.tickSize(-height)
.tickFormat("")
)
樣式
#grid .tick:nth-child(2) {
display: none
}
#grid path {
display: none
}
效果展示
繪制圖形
繪制網(wǎng)線的時(shí)候我們使用了直線生成器,這里簡(jiǎn)要介紹一下直線生成器耸别。
首先,在svg中县钥,線段元素放的寫(xiě)法是
<line x1="20" y1="20" x2="100" y2="100" />
或者是
<path d="M20,20L100,100>
兩個(gè)點(diǎn)形成的一條線段很容易手寫(xiě)生成出來(lái)秀姐,但是如果有成千上萬(wàn)個(gè)點(diǎn)就很不方便了,因此d3 引入了 路徑生成器 這個(gè)概念魁蒜,能夠自動(dòng)根據(jù)數(shù)據(jù)生成路徑囊扳。用于生成線段的生成器也就叫做 線段生成器吩翻。
例如:
var lines = [ [80, 80], [200, 100], [200, 200], [100, 200] ]
var linePath = d3.line()
svg.append("path")
.attr("d", linePath(lines))
.attr("stroke-width", "2px")
.attr("stroke", "red")
.attr("fill", "none")
生成效果
了解了這個(gè)知識(shí)點(diǎn)之后,繼續(xù)繪制折線圖
//創(chuàng)建一個(gè)直線生成器
var linePath = d3.line()
.curve(d3.curveCardinal.tension(0.5))
.x( function(d){ return xScale(d[0]) })
.y( function(d){ return yScale(d[1])})
var colors = ["rgb(0, 188, 212)", "rgb(255, 64, 129)"]
//添加路徑
svg.append("g").selectAll("path")
.data(data)
.enter()
.append("path")
.attr("transform","translate(0, 0)")
.attr("d", function(d){
return linePath(d.gdp)
})
.attr("fill", "none")
.attr("stroke-width", "2px")
.attr("stroke", function(d, i){
return colors[i]
})
添加路徑之后的效果
添加左側(cè)指示欄
類(lèi)別指示欄在上一節(jié)已經(jīng)介紹過(guò)了锥咸,這一節(jié)也就不過(guò)多介紹狭瞎,代碼示下
var cover =svg.append("g")
cover.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("width", 10)
.attr("height", 10)
.attr("fill", function(d, i){
return i%2 == 0 ? colors[0] : colors[1]
})
.attr("transform", function(d, i){
return `translate(10, ${(i)*20})`
})
cover.selectAll("text")
.data(data)
.enter()
.append("text")
.text(function(d, i){
return d.country
})
.attr("transform", function(d, i){
return `translate(27, ${(i)*20})`
})
.attr("font-size", '12px')
.attr("dy",function(){
return '0.75em'
})
.attr("fill", function(){
return '#333'
})
此時(shí)的效果
添加提示欄和準(zhǔn)線
截至到這里,已經(jīng)完成了折線圖的基本制作搏予。接下來(lái)添加一些交互效果熊锭。
- 滑動(dòng)準(zhǔn)線
- 內(nèi)容提示框
最后要達(dá)到的效果,鼠標(biāo)在圖表移動(dòng)時(shí)雪侥,準(zhǔn)線吸附到最近的參考線碗殷,并且提示欄內(nèi)顯示該參考線位置上折線的數(shù)據(jù)。
其實(shí)思路很簡(jiǎn)單速缨,只要計(jì)算出每?jī)蓚€(gè)參考線之間的距離singleStep锌妻,就可以根據(jù)鼠標(biāo)位置找到當(dāng)前鼠標(biāo)距離哪兩個(gè)參考線之間,并且距離哪個(gè)參考線更近旬牲,判斷出來(lái)之后準(zhǔn)線就吸到相應(yīng)的參考線仿粹。
這里采用了mousemove事件來(lái)實(shí)時(shí)判斷當(dāng)前鼠標(biāo)位置并進(jìn)行運(yùn)算,事實(shí)上原茅,并不需要實(shí)時(shí)觸發(fā)這個(gè)函數(shù)吭历,準(zhǔn)線吸附速度只要流暢就可以,大量觸發(fā)會(huì)極大浪費(fèi)計(jì)算機(jī)性能擂橘。這里采用了一個(gè)高級(jí)函數(shù)晌区,通過(guò)控制函數(shù)在多少毫秒內(nèi)只執(zhí)行一次,來(lái)幫助解決這個(gè)問(wèn)題通贞。節(jié)流函數(shù)詳細(xì)講解在這里
節(jié)流函數(shù)
//節(jié)流函數(shù)
var throttle = function (fn, interval) {
var timer, firstTime = true;
return function () {
var args = arguments;
var _me = this;
if ( firstTime ) {
fn.apply(_me, args)
return firstTime = false;
}
if ( timer ) {
return false
}
timer = setTimeout(function () {
clearTimeout(timer);
timer=null;
fn.apply(_me, args)
}, interval || 500)
}
}
繪制提示欄和準(zhǔn)線(無(wú)刻度的y軸)
var detailLine = svg.append("g")
.attr("class","line_y")
.attr("transform","translate(" + width + "," + 0 + ")")
.call(yAxis.ticks(0).tickSize(0).tickFormat(""));
//添加提示欄
var tooltip = d3.select("body")
.append("div")
.attr("class", "tooltip")
.style("opacity", 0)
//計(jì)算位置 便于吸附
let singleStep = width / (years.length-1)
//這里使用節(jié)流函數(shù)朗若,避免過(guò)多運(yùn)算導(dǎo)致瀏覽器卡頓
document.getElementById('chart').onmousemove =throttle(function(e){
console.log(e.offsetX)
e.stopPropagation();
let t = Math.round((e.offsetX - padding.left) / singleStep)*singleStep
detailLine.attr("transform","translate(" + t + "," + 0 + ")")
let year = _xScale(t)
let currentHtml = []
data.forEach( (e) => {
e.gdp.forEach( (ev, i) =>{
if(ev[0]==year){
currentHtml.push(`<div>${e.country}: ${ev[1]}</div>`);
}
})
})
currentHtml.unshift(`<div>${year}</div>`)
tooltip.html(currentHtml.join(""))
.style("left", e.pageX + 20+ "px")
.style("top", e.pageY + 20 + "px")
.style("opacity", 1)
},50)
//隱藏顯示欄
document.onclick= function(){
tooltip.style("opacity", 0)
}
提示欄樣式
.tooltip {
position: absolute;
min-width: 100px;
height: auto;
font-size: 14px;
text-align: center;
border: 1px solid #666;
border-radius: 5px;
color: #fff;
background:rgba(0, 0, 0, 0.8);
padding-bottom: 5px;
transition: transform 0.2s
}
最終效果